cwadayi commited on
Commit
326137f
Β·
verified Β·
1 Parent(s): e6a8aae

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +621 -98
app.py CHANGED
@@ -1,123 +1,646 @@
 
 
 
 
 
 
 
 
1
  import sqlite3
2
  import pandas as pd
3
- import gradio as gr
4
  import matplotlib.pyplot as plt
5
  import cartopy.crs as ccrs
6
  import cartopy.feature as cfeature
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
- # --- εΎŒη«―θ³‡ζ–™ζŸ₯θ©’θˆ‡ηΉͺεœ–ε‡½εΌ ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  async def fetch_and_plot_data(
10
  start_date, start_time, end_date, end_time,
11
  lat_min, lat_max, lon_min, lon_max,
12
  depth_min, depth_max, ML_min, ML_max
13
- ):
 
14
  try:
15
- # η΅„εˆζ—₯ζœŸε’Œζ™‚ι–“
16
- start_datetime_str = f"{start_date.strip()} {start_time.strip() if start_time and start_time.strip() else '00:00:00'}"
17
- end_datetime_str = f"{end_date.strip()} {end_time.strip() if end_time and end_time.strip() else '23:59:59'}"
18
-
19
- # ι€£ζŽ₯εˆ°θ³‡ζ–™εΊ«
20
- conn = sqlite3.connect('earthquake_data.db')
21
 
22
- # ε»Ίη«‹ζŸ₯θ©’
23
- query = "SELECT * FROM earthquakes WHERE (date || ' ' || time) BETWEEN ? AND ?"
24
- params = [start_datetime_str, end_datetime_str]
25
-
26
- # 處理兢他篩選撝仢
27
- filters = {
28
- "lat BETWEEN ? AND ?": (lat_min, lat_max),
29
- "lon BETWEEN ? AND ?": (lon_min, lon_max),
30
- "depth BETWEEN ? AND ?": (depth_min, depth_max),
31
- "ML BETWEEN ? AND ?": (ML_min, ML_max),
32
- }
33
 
34
- for condition, values in filters.items():
35
- if values[0] is not None and values[1] is not None:
36
- query += f" AND {condition}"
37
- params.extend(values)
38
-
39
- # 執葌ζŸ₯θ©’
40
- df = pd.read_sql_query(query, conn, params=tuple(params))
41
- conn.close()
42
-
43
- # ε¦‚ζžœζ²’ζœ‰θ³‡ζ–™οΌŒε›žε‚³η©Ίηš„ DataFrame ε’Œ None
44
- if df.empty:
45
- return pd.DataFrame({"Message": ["No data found for the selected filters."]}), None
46
-
47
- # --- ηΉͺεœ– ---
48
- fig = plt.figure(figsize=(10, 12))
49
- ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree())
50
- ax.set_extent([lon_min, lon_max, lat_min, lat_max], crs=ccrs.PlateCarree())
51
-
52
- # 加ε…₯εœ°εœ–η‰ΉεΎ΅
53
- ax.add_feature(cfeature.LAND, edgecolor='black')
54
- ax.add_feature(cfeature.OCEAN)
55
- ax.add_feature(cfeature.COASTLINE)
56
- ax.add_feature(cfeature.BORDERS, linestyle=':')
57
-
58
- # ηΉͺθ£½ζ•£ι»žεœ–
59
- scatter = ax.scatter(
60
- df['lon'], df['lat'], c=df['ML'],
61
- cmap='viridis', alpha=0.7, s=50,
62
- transform=ccrs.PlateCarree()
63
  )
64
 
65
- # 加ε…₯ι‘θ‰²ζ’ε’Œζ¨™ι‘Œ
66
- plt.colorbar(scatter, ax=ax, orientation='vertical', label='Magnitude (ML)', shrink=0.6)
67
- ax.set_title(f'Earthquake Distribution on Map\n({start_date} to {end_date})')
68
 
69
- # ι—œι–‰εœ–ε½’δ»₯ιΏε…εœ¨δΌΊζœε™¨δΈŠι‘―η€Ί
70
- plt.close(fig)
 
71
 
72
- return df, fig
 
 
 
 
 
 
 
73
 
 
 
 
 
 
 
74
  except Exception as e:
75
- # ε›žε‚³ιŒ―θͺ€θ¨Šζ―ε’Œ None
76
- return pd.DataFrame({"Error": [str(e)]}), None
 
 
 
 
77
 
 
 
 
 
 
 
 
 
78
 
79
- # --- Gradio 使用者介青 ---
80
- with gr.Blocks() as demo:
81
- gr.Markdown("# Earthquake Data Explorer")
82
- gr.Markdown("Use the filters below to search the earthquake catalog and visualize the distribution.")
83
 
84
- with gr.Row():
85
- with gr.Column(scale=1):
86
- gr.Markdown("### Date & Time Range")
87
- start_date_input = gr.Textbox(label="Start Date", value="2024-01-01")
88
- start_time_input = gr.Textbox(label="Start Time (HH:MM:SS)", placeholder="00:00:00")
89
- end_date_input = gr.Textbox(label="End Date", value="2024-12-31")
90
- end_time_input = gr.Textbox(label="End Time (HH:MM:SS)", placeholder="23:59:59")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
- with gr.Column(scale=1):
93
- gr.Markdown("### Geographical & Physical Filters")
94
- lon_min_input = gr.Number(label="Longitude From", value=119)
95
- lon_max_input = gr.Number(label="To", value=123)
96
- lat_min_input = gr.Number(label="Latitude From", value=21)
97
- lat_max_input = gr.Number(label="To", value=26)
98
- depth_min_input = gr.Number(label="Depth From", value=0)
99
- depth_max_input = gr.Number(label="To", value=100)
100
- ML_min_input = gr.Number(label="Magnitude From", value=4.5)
101
- ML_max_input = gr.Number(label="To", value=8)
102
-
103
- filter_button = gr.Button("Filter and Plot Data", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
- with gr.Row():
106
- with gr.Column(scale=2):
107
- output_plot = gr.Plot(label="Earthquake Distribution Map")
108
- with gr.Column(scale=3):
109
- output_df = gr.DataFrame(label="Filtered Results")
110
 
111
- filter_button.click(
112
- fn=fetch_and_plot_data,
113
- inputs=[
114
- start_date_input, start_time_input, end_date_input, end_time_input,
115
- lat_min_input, lat_max_input, lon_min_input, lon_max_input,
116
- depth_min_input, depth_max_input, ML_min_input, ML_max_input
117
- ],
118
- outputs=[output_df, output_plot]
119
- )
120
-
121
- # --- δΈ»η¨‹εΌεŸ·θ‘Œ ---
122
- if __name__ == "__main__":
123
- demo.launch(server_name="0.0.0.0")
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Earthquake Data MCP Server for Hugging Face Spaces
4
+ Provides earthquake data querying and visualization capabilities via both Gradio UI and MCP protocol.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
  import sqlite3
10
  import pandas as pd
 
11
  import matplotlib.pyplot as plt
12
  import cartopy.crs as ccrs
13
  import cartopy.feature as cfeature
14
+ import base64
15
+ import io
16
+ from datetime import datetime
17
+ from typing import Any, Dict, List, Optional, Tuple
18
+ import gradio as gr
19
+ import logging
20
+ import threading
21
+ import queue
22
+ import time
23
+
24
+ # Configure matplotlib for non-interactive backend
25
+ plt.switch_backend('Agg')
26
+
27
+ # Configure logging
28
+ logging.basicConfig(level=logging.INFO)
29
+ logger = logging.getLogger(__name__)
30
+
31
+ class EarthquakeDataService:
32
+ """Core earthquake data service used by both Gradio and MCP interfaces."""
33
+
34
+ def __init__(self, db_path: str = 'earthquake_data.db'):
35
+ self.db_path = db_path
36
+
37
+ async def query_earthquakes_data(self,
38
+ start_date: str = "2024-01-01",
39
+ start_time: str = "00:00:00",
40
+ end_date: str = "2024-12-31",
41
+ end_time: str = "23:59:59",
42
+ lat_min: float = 21,
43
+ lat_max: float = 26,
44
+ lon_min: float = 119,
45
+ lon_max: float = 123,
46
+ depth_min: float = 0,
47
+ depth_max: float = 100,
48
+ ML_min: float = 4.5,
49
+ ML_max: float = 8,
50
+ limit: int = 1000) -> Dict[str, Any]:
51
+ """Query earthquake data with filters."""
52
+ try:
53
+ # Combine date and time
54
+ start_datetime_str = f"{start_date.strip()} {start_time.strip()}"
55
+ end_datetime_str = f"{end_date.strip()} {end_time.strip()}"
56
+
57
+ # Connect to database
58
+ conn = sqlite3.connect(self.db_path)
59
+
60
+ # Build query
61
+ query = "SELECT * FROM earthquakes WHERE (date || ' ' || time) BETWEEN ? AND ?"
62
+ params = [start_datetime_str, end_datetime_str]
63
+
64
+ # Add filters
65
+ filters = {
66
+ "lat BETWEEN ? AND ?": (lat_min, lat_max),
67
+ "lon BETWEEN ? AND ?": (lon_min, lon_max),
68
+ "depth BETWEEN ? AND ?": (depth_min, depth_max),
69
+ "ML BETWEEN ? AND ?": (ML_min, ML_max),
70
+ }
71
+
72
+ for condition, values in filters.items():
73
+ if values[0] is not None and values[1] is not None:
74
+ query += f" AND {condition}"
75
+ params.extend(values)
76
+
77
+ query += f" LIMIT {limit}"
78
+
79
+ # Execute query
80
+ df = pd.read_sql_query(query, conn, params=tuple(params))
81
+ conn.close()
82
+
83
+ if df.empty:
84
+ return {
85
+ "success": True,
86
+ "message": "No data found for the selected filters",
87
+ "count": 0,
88
+ "data": [],
89
+ "dataframe": pd.DataFrame({"Message": ["No data found for the selected filters."]})
90
+ }
91
+
92
+ # Convert to list of dictionaries for MCP
93
+ data = df.to_dict('records')
94
+
95
+ return {
96
+ "success": True,
97
+ "count": len(data),
98
+ "data": data,
99
+ "dataframe": df,
100
+ "summary": {
101
+ "magnitude_range": [float(df['ML'].min()), float(df['ML'].max())],
102
+ "depth_range": [float(df['depth'].min()), float(df['depth'].max())],
103
+ "location_bounds": {
104
+ "lat_range": [float(df['lat'].min()), float(df['lat'].max())],
105
+ "lon_range": [float(df['lon'].min()), float(df['lon'].max())]
106
+ }
107
+ }
108
+ }
109
+
110
+ except Exception as e:
111
+ logger.error(f"Error querying earthquakes: {e}")
112
+ return {
113
+ "success": False,
114
+ "error": str(e),
115
+ "dataframe": pd.DataFrame({"Error": [str(e)]})
116
+ }
117
+
118
+ async def create_earthquake_map(self,
119
+ start_date: str = "2024-01-01",
120
+ start_time: str = "00:00:00",
121
+ end_date: str = "2024-12-31",
122
+ end_time: str = "23:59:59",
123
+ lat_min: float = 21,
124
+ lat_max: float = 26,
125
+ lon_min: float = 119,
126
+ lon_max: float = 123,
127
+ depth_min: float = 0,
128
+ depth_max: float = 100,
129
+ ML_min: float = 4.5,
130
+ ML_max: float = 8,
131
+ return_base64: bool = False) -> Dict[str, Any]:
132
+ """Create earthquake distribution map."""
133
+ try:
134
+ # Get earthquake data first
135
+ query_result = await self.query_earthquakes_data(
136
+ start_date, start_time, end_date, end_time,
137
+ lat_min, lat_max, lon_min, lon_max,
138
+ depth_min, depth_max, ML_min, ML_max
139
+ )
140
+
141
+ if not query_result.get("success") or query_result.get("count", 0) == 0:
142
+ return {"success": False, "error": "No data available for mapping", "figure": None}
143
+
144
+ df = query_result["dataframe"]
145
+
146
+ # Create map
147
+ fig = plt.figure(figsize=(12, 10))
148
+ ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree())
149
+ ax.set_extent([lon_min, lon_max, lat_min, lat_max], crs=ccrs.PlateCarree())
150
+
151
+ # Add map features
152
+ ax.add_feature(cfeature.LAND, edgecolor='black', alpha=0.8)
153
+ ax.add_feature(cfeature.OCEAN, alpha=0.6)
154
+ ax.add_feature(cfeature.COASTLINE, linewidth=0.8)
155
+ ax.add_feature(cfeature.BORDERS, linestyle=':', alpha=0.7)
156
+
157
+ # Create scatter plot
158
+ scatter = ax.scatter(
159
+ df['lon'], df['lat'], c=df['ML'],
160
+ cmap='viridis', alpha=0.7, s=60,
161
+ transform=ccrs.PlateCarree(),
162
+ edgecolors='black', linewidth=0.5
163
+ )
164
+
165
+ # Add colorbar and title
166
+ plt.colorbar(scatter, ax=ax, orientation='vertical',
167
+ label='Magnitude (ML)', shrink=0.7, pad=0.05)
168
+ ax.set_title(f'Earthquake Distribution Map\n'
169
+ f'{start_date} to {end_date} ({len(df)} events)',
170
+ fontsize=14, pad=20)
171
+
172
+ # Add gridlines
173
+ ax.gridlines(draw_labels=True, alpha=0.5)
174
+
175
+ result = {
176
+ "success": True,
177
+ "earthquake_count": len(df),
178
+ "date_range": f"{start_date} to {end_date}",
179
+ "bounds": {
180
+ "lat_min": lat_min, "lat_max": lat_max,
181
+ "lon_min": lon_min, "lon_max": lon_max
182
+ },
183
+ "figure": fig
184
+ }
185
+
186
+ if return_base64:
187
+ # Convert to base64
188
+ buffer = io.BytesIO()
189
+ plt.savefig(buffer, format='png', dpi=150, bbox_inches='tight')
190
+ buffer.seek(0)
191
+ image_base64 = base64.b64encode(buffer.getvalue()).decode()
192
+ result["map_image_base64"] = image_base64
193
+ buffer.close()
194
+
195
+ return result
196
+
197
+ except Exception as e:
198
+ logger.error(f"Error creating earthquake map: {e}")
199
+ return {"success": False, "error": str(e), "figure": None}
200
+
201
+ async def get_earthquake_stats(self,
202
+ start_date: str = "2024-01-01",
203
+ end_date: str = "2024-12-31",
204
+ lat_min: float = 21,
205
+ lat_max: float = 26,
206
+ lon_min: float = 119,
207
+ lon_max: float = 123) -> Dict[str, Any]:
208
+ """Get statistical summary of earthquake data."""
209
+ try:
210
+ query_result = await self.query_earthquakes_data(
211
+ start_date=start_date, end_date=end_date,
212
+ lat_min=lat_min, lat_max=lat_max,
213
+ lon_min=lon_min, lon_max=lon_max
214
+ )
215
+
216
+ if not query_result.get("success") or query_result.get("count", 0) == 0:
217
+ return {"success": False, "error": "No data available for statistics"}
218
 
219
+ df = query_result["dataframe"]
220
+
221
+ stats = {
222
+ "success": True,
223
+ "total_earthquakes": len(df),
224
+ "date_range": {"start": start_date, "end": end_date},
225
+ "magnitude_stats": {
226
+ "min": float(df['ML'].min()),
227
+ "max": float(df['ML'].max()),
228
+ "mean": float(df['ML'].mean()),
229
+ "median": float(df['ML'].median()),
230
+ "std": float(df['ML'].std())
231
+ },
232
+ "depth_stats": {
233
+ "min": float(df['depth'].min()),
234
+ "max": float(df['depth'].max()),
235
+ "mean": float(df['depth'].mean()),
236
+ "median": float(df['depth'].median())
237
+ },
238
+ "magnitude_distribution": {
239
+ "4.0-4.9": len(df[(df['ML'] >= 4.0) & (df['ML'] < 5.0)]),
240
+ "5.0-5.9": len(df[(df['ML'] >= 5.0) & (df['ML'] < 6.0)]),
241
+ "6.0-6.9": len(df[(df['ML'] >= 6.0) & (df['ML'] < 7.0)]),
242
+ "7.0+": len(df[df['ML'] >= 7.0])
243
+ },
244
+ "depth_distribution": {
245
+ "shallow (0-10km)": len(df[(df['depth'] >= 0) & (df['depth'] <= 10)]),
246
+ "intermediate (10-70km)": len(df[(df['depth'] > 10) & (df['depth'] <= 70)]),
247
+ "deep (>70km)": len(df[df['depth'] > 70])
248
+ }
249
+ }
250
+
251
+ return stats
252
+
253
+ except Exception as e:
254
+ logger.error(f"Error getting earthquake stats: {e}")
255
+ return {"success": False, "error": str(e)}
256
+
257
+
258
+ class EarthquakeMCPServer:
259
+ """MCP Server component for handling MCP protocol requests."""
260
+
261
+ def __init__(self, data_service: EarthquakeDataService):
262
+ self.data_service = data_service
263
+ self.tools = {
264
+ "query_earthquakes": {
265
+ "description": "Query earthquake data with various filters",
266
+ "parameters": {
267
+ "type": "object",
268
+ "properties": {
269
+ "start_date": {"type": "string", "description": "Start date (YYYY-MM-DD)", "default": "2024-01-01"},
270
+ "start_time": {"type": "string", "description": "Start time (HH:MM:SS)", "default": "00:00:00"},
271
+ "end_date": {"type": "string", "description": "End date (YYYY-MM-DD)", "default": "2024-12-31"},
272
+ "end_time": {"type": "string", "description": "End time (HH:MM:SS)", "default": "23:59:59"},
273
+ "lat_min": {"type": "number", "description": "Minimum latitude", "default": 21},
274
+ "lat_max": {"type": "number", "description": "Maximum latitude", "default": 26},
275
+ "lon_min": {"type": "number", "description": "Minimum longitude", "default": 119},
276
+ "lon_max": {"type": "number", "description": "Maximum longitude", "default": 123},
277
+ "depth_min": {"type": "number", "description": "Minimum depth (km)", "default": 0},
278
+ "depth_max": {"type": "number", "description": "Maximum depth (km)", "default": 100},
279
+ "ML_min": {"type": "number", "description": "Minimum magnitude", "default": 4.5},
280
+ "ML_max": {"type": "number", "description": "Maximum magnitude", "default": 8},
281
+ "limit": {"type": "integer", "description": "Maximum results", "default": 1000}
282
+ }
283
+ }
284
+ },
285
+ "create_earthquake_map": {
286
+ "description": "Create a map visualization of earthquake data",
287
+ "parameters": {
288
+ "type": "object",
289
+ "properties": {
290
+ "start_date": {"type": "string", "default": "2024-01-01"},
291
+ "start_time": {"type": "string", "default": "00:00:00"},
292
+ "end_date": {"type": "string", "default": "2024-12-31"},
293
+ "end_time": {"type": "string", "default": "23:59:59"},
294
+ "lat_min": {"type": "number", "default": 21},
295
+ "lat_max": {"type": "number", "default": 26},
296
+ "lon_min": {"type": "number", "default": 119},
297
+ "lon_max": {"type": "number", "default": 123},
298
+ "depth_min": {"type": "number", "default": 0},
299
+ "depth_max": {"type": "number", "default": 100},
300
+ "ML_min": {"type": "number", "default": 4.5},
301
+ "ML_max": {"type": "number", "default": 8},
302
+ "return_base64": {"type": "boolean", "default": True}
303
+ }
304
+ }
305
+ },
306
+ "get_earthquake_stats": {
307
+ "description": "Get statistical summary of earthquake data",
308
+ "parameters": {
309
+ "type": "object",
310
+ "properties": {
311
+ "start_date": {"type": "string", "default": "2024-01-01"},
312
+ "end_date": {"type": "string", "default": "2024-12-31"},
313
+ "lat_min": {"type": "number", "default": 21},
314
+ "lat_max": {"type": "number", "default": 26},
315
+ "lon_min": {"type": "number", "default": 119},
316
+ "lon_max": {"type": "number", "default": 123}
317
+ }
318
+ }
319
+ }
320
+ }
321
+
322
+ async def handle_mcp_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
323
+ """Handle MCP protocol requests."""
324
+ try:
325
+ method = request.get("method")
326
+ params = request.get("params", {})
327
+
328
+ if method == "initialize":
329
+ return {
330
+ "jsonrpc": "2.0",
331
+ "id": request.get("id"),
332
+ "result": {
333
+ "protocolVersion": "2024-11-05",
334
+ "capabilities": {"tools": {}},
335
+ "serverInfo": {
336
+ "name": "earthquake-data-server",
337
+ "version": "1.0.0"
338
+ }
339
+ }
340
+ }
341
+
342
+ elif method == "tools/list":
343
+ return {
344
+ "jsonrpc": "2.0",
345
+ "id": request.get("id"),
346
+ "result": {
347
+ "tools": [
348
+ {"name": name, **tool_info}
349
+ for name, tool_info in self.tools.items()
350
+ ]
351
+ }
352
+ }
353
+
354
+ elif method == "tools/call":
355
+ tool_name = params.get("name")
356
+ arguments = params.get("arguments", {})
357
+
358
+ if tool_name == "query_earthquakes":
359
+ result = await self.data_service.query_earthquakes_data(**arguments)
360
+ # Remove dataframe for JSON serialization
361
+ result.pop("dataframe", None)
362
+ elif tool_name == "create_earthquake_map":
363
+ result = await self.data_service.create_earthquake_map(**arguments)
364
+ # Remove figure for JSON serialization
365
+ result.pop("figure", None)
366
+ elif tool_name == "get_earthquake_stats":
367
+ result = await self.data_service.get_earthquake_stats(**arguments)
368
+ else:
369
+ return {
370
+ "jsonrpc": "2.0",
371
+ "id": request.get("id"),
372
+ "error": {"code": -32601, "message": f"Unknown tool: {tool_name}"}
373
+ }
374
+
375
+ return {
376
+ "jsonrpc": "2.0",
377
+ "id": request.get("id"),
378
+ "result": {
379
+ "content": [
380
+ {
381
+ "type": "text",
382
+ "text": json.dumps(result, indent=2, ensure_ascii=False)
383
+ }
384
+ ]
385
+ }
386
+ }
387
+
388
+ else:
389
+ return {
390
+ "jsonrpc": "2.0",
391
+ "id": request.get("id"),
392
+ "error": {"code": -32601, "message": f"Unknown method: {method}"}
393
+ }
394
+
395
+ except Exception as e:
396
+ logger.error(f"Error handling MCP request: {e}")
397
+ return {
398
+ "jsonrpc": "2.0",
399
+ "id": request.get("id", 0),
400
+ "error": {"code": -32603, "message": str(e)}
401
+ }
402
+
403
+
404
+ # Global data service instance
405
+ data_service = EarthquakeDataService()
406
+ mcp_server = EarthquakeMCPServer(data_service)
407
+
408
+ # --- Gradio Interface Functions ---
409
  async def fetch_and_plot_data(
410
  start_date, start_time, end_date, end_time,
411
  lat_min, lat_max, lon_min, lon_max,
412
  depth_min, depth_max, ML_min, ML_max
413
+ ) -> Tuple[pd.DataFrame, Any]:
414
+ """Gradio interface function for fetching and plotting data."""
415
  try:
416
+ # Query data
417
+ query_result = await data_service.query_earthquakes_data(
418
+ start_date, start_time, end_date, end_time,
419
+ lat_min, lat_max, lon_min, lon_max,
420
+ depth_min, depth_max, ML_min, ML_max
421
+ )
422
 
423
+ df = query_result.get("dataframe", pd.DataFrame())
 
 
 
 
 
 
 
 
 
 
424
 
425
+ if query_result.get("count", 0) == 0:
426
+ return df, None
427
+
428
+ # Create map
429
+ map_result = await data_service.create_earthquake_map(
430
+ start_date, start_time, end_date, end_time,
431
+ lat_min, lat_max, lon_min, lon_max,
432
+ depth_min, depth_max, ML_min, ML_max
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  )
434
 
435
+ figure = map_result.get("figure")
436
+ return df, figure
 
437
 
438
+ except Exception as e:
439
+ logger.error(f"Error in fetch_and_plot_data: {e}")
440
+ return pd.DataFrame({"Error": [str(e)]}), None
441
 
442
+ def gradio_fetch_and_plot_data(*args):
443
+ """Synchronous wrapper for Gradio."""
444
+ loop = asyncio.new_event_loop()
445
+ asyncio.set_event_loop(loop)
446
+ try:
447
+ return loop.run_until_complete(fetch_and_plot_data(*args))
448
+ finally:
449
+ loop.close()
450
 
451
+ async def handle_mcp_request_gradio(request_json: str) -> str:
452
+ """Handle MCP requests through Gradio interface."""
453
+ try:
454
+ request = json.loads(request_json)
455
+ response = await mcp_server.handle_mcp_request(request)
456
+ return json.dumps(response, indent=2, ensure_ascii=False)
457
  except Exception as e:
458
+ error_response = {
459
+ "jsonrpc": "2.0",
460
+ "id": None,
461
+ "error": {"code": -32700, "message": f"Parse error: {str(e)}"}
462
+ }
463
+ return json.dumps(error_response, indent=2)
464
 
465
+ def gradio_handle_mcp_request(request_json: str) -> str:
466
+ """Synchronous wrapper for MCP requests in Gradio."""
467
+ loop = asyncio.new_event_loop()
468
+ asyncio.set_event_loop(loop)
469
+ try:
470
+ return loop.run_until_complete(handle_mcp_request_gradio(request_json))
471
+ finally:
472
+ loop.close()
473
 
474
+ # --- Gradio Interface ---
475
+ with gr.Blocks(title="Earthquake Data MCP Server", theme=gr.themes.Soft()) as app:
476
+ gr.Markdown("# 🌍 Earthquake Data Explorer & MCP Server")
477
+ gr.Markdown("This application provides both a web interface and MCP server for earthquake data analysis.")
478
 
479
+ with gr.Tabs():
480
+ # Data Explorer Tab
481
+ with gr.TabItem("πŸ“Š Data Explorer"):
482
+ gr.Markdown("### Use the filters below to search the earthquake catalog and visualize the distribution.")
483
+
484
+ with gr.Row():
485
+ with gr.Column(scale=1):
486
+ gr.Markdown("#### Date & Time Range")
487
+ start_date_input = gr.Textbox(label="Start Date", value="2024-01-01")
488
+ start_time_input = gr.Textbox(label="Start Time (HH:MM:SS)", placeholder="00:00:00")
489
+ end_date_input = gr.Textbox(label="End Date", value="2024-12-31")
490
+ end_time_input = gr.Textbox(label="End Time (HH:MM:SS)", placeholder="23:59:59")
491
+
492
+ with gr.Column(scale=1):
493
+ gr.Markdown("#### Geographical & Physical Filters")
494
+ lon_min_input = gr.Number(label="Longitude From", value=119)
495
+ lon_max_input = gr.Number(label="To", value=123)
496
+ lat_min_input = gr.Number(label="Latitude From", value=21)
497
+ lat_max_input = gr.Number(label="To", value=26)
498
+ depth_min_input = gr.Number(label="Depth From", value=0)
499
+ depth_max_input = gr.Number(label="To", value=100)
500
+ ML_min_input = gr.Number(label="Magnitude From", value=4.5)
501
+ ML_max_input = gr.Number(label="To", value=8)
502
+
503
+ filter_button = gr.Button("πŸ” Filter and Plot Data", variant="primary", size="lg")
504
+
505
+ with gr.Row():
506
+ with gr.Column(scale=2):
507
+ output_plot = gr.Plot(label="Earthquake Distribution Map")
508
+ with gr.Column(scale=3):
509
+ output_df = gr.DataFrame(label="Filtered Results")
510
+
511
+ filter_button.click(
512
+ fn=gradio_fetch_and_plot_data,
513
+ inputs=[
514
+ start_date_input, start_time_input, end_date_input, end_time_input,
515
+ lat_min_input, lat_max_input, lon_min_input, lon_max_input,
516
+ depth_min_input, depth_max_input, ML_min_input, ML_max_input
517
+ ],
518
+ outputs=[output_df, output_plot]
519
+ )
520
+
521
+ # MCP Interface Tab
522
+ with gr.TabItem("πŸ”Œ MCP Interface"):
523
+ gr.Markdown("### Model Context Protocol (MCP) Interface")
524
+ gr.Markdown("""
525
+ This tab allows you to interact with the MCP server directly. Send JSON-RPC requests to test the MCP functionality.
526
+
527
+ **Available Methods:**
528
+ - `initialize`: Initialize the MCP connection
529
+ - `tools/list`: List available tools
530
+ - `tools/call`: Call a specific tool
531
+
532
+ **Available Tools:**
533
+ - `query_earthquakes`: Query earthquake data
534
+ - `create_earthquake_map`: Create earthquake maps
535
+ - `get_earthquake_stats`: Get earthquake statistics
536
+ """)
537
+
538
+ with gr.Row():
539
+ with gr.Column():
540
+ mcp_request_input = gr.Code(
541
+ label="MCP Request (JSON-RPC)",
542
+ language="json",
543
+ value='{\n "jsonrpc": "2.0",\n "id": 1,\n "method": "tools/list",\n "params": {}\n}'
544
+ )
545
+ mcp_submit_button = gr.Button("πŸ“€ Send MCP Request", variant="primary")
546
+
547
+ with gr.Column():
548
+ mcp_response_output = gr.Code(
549
+ label="MCP Response",
550
+ language="json"
551
+ )
552
+
553
+ mcp_submit_button.click(
554
+ fn=gradio_handle_mcp_request,
555
+ inputs=[mcp_request_input],
556
+ outputs=[mcp_response_output]
557
+ )
558
+
559
+ # Example requests
560
+ gr.Markdown("#### Example Requests:")
561
+
562
+ example_requests = [
563
+ ("Initialize Connection", '{\n "jsonrpc": "2.0",\n "id": 1,\n "method": "initialize",\n "params": {\n "protocolVersion": "2024-11-05",\n "capabilities": {},\n "clientInfo": {"name": "web-client", "version": "1.0.0"}\n }\n}'),
564
+ ("List Tools", '{\n "jsonrpc": "2.0",\n "id": 2,\n "method": "tools/list",\n "params": {}\n}'),
565
+ ("Query Earthquakes", '{\n "jsonrpc": "2.0",\n "id": 3,\n "method": "tools/call",\n "params": {\n "name": "query_earthquakes",\n "arguments": {\n "start_date": "2024-01-01",\n "end_date": "2024-01-31",\n "ML_min": 5.0,\n "limit": 10\n }\n }\n}'),
566
+ ("Get Statistics", '{\n "jsonrpc": "2.0",\n "id": 4,\n "method": "tools/call",\n "params": {\n "name": "get_earthquake_stats",\n "arguments": {\n "start_date": "2024-01-01",\n "end_date": "2024-03-31"\n }\n }\n}')
567
+ ]
568
+
569
+ for title, request in example_requests:
570
+ with gr.Accordion(title, open=False):
571
+ gr.Code(value=request, language="json")
572
 
573
+ # API Documentation Tab
574
+ with gr.TabItem("πŸ“š API Documentation"):
575
+ gr.Markdown("""
576
+ # API Documentation
577
+
578
+ ## MCP Server Endpoint
579
+
580
+ **URL:** `https://your-space-name.hf.space` (when deployed)
581
+
582
+ ## Available Tools
583
+
584
+ ### 1. query_earthquakes
585
+ Query earthquake data with various filters.
586
+
587
+ **Parameters:**
588
+ - `start_date` (string): Start date in YYYY-MM-DD format
589
+ - `start_time` (string): Start time in HH:MM:SS format
590
+ - `end_date` (string): End date in YYYY-MM-DD format
591
+ - `end_time` (string): End time in HH:MM:SS format
592
+ - `lat_min`, `lat_max` (number): Latitude range
593
+ - `lon_min`, `lon_max` (number): Longitude range
594
+ - `depth_min`, `depth_max` (number): Depth range in km
595
+ - `ML_min`, `ML_max` (number): Magnitude range
596
+ - `limit` (integer): Maximum number of results
597
+
598
+ ### 2. create_earthquake_map
599
+ Create a visualization map of earthquake data.
600
+
601
+ **Parameters:** Same as query_earthquakes plus:
602
+ - `return_base64` (boolean): Return map as base64 encoded image
603
+
604
+ ### 3. get_earthquake_stats
605
+ Get statistical summary of earthquake data.
606
+
607
+ **Parameters:**
608
+ - `start_date`, `end_date` (string): Date range
609
+ - `lat_min`, `lat_max`, `lon_min`, `lon_max` (number): Geographic bounds
610
+
611
+ ## Integration with AI Assistants
612
+
613
+ To use this MCP server with Claude Desktop or other MCP clients, add this configuration:
614
+
615
+ ```json
616
+ {
617
+ "mcpServers": {
618
+ "earthquake-data": {
619
+ "command": "python",
620
+ "args": ["path/to/earthquake_mcp_server.py"],
621
+ "env": {
622
+ "EARTHQUAKE_DB_PATH": "earthquake_data.db"
623
+ }
624
+ }
625
+ }
626
+ }
627
+ ```
628
+
629
+ For web-based integration, make HTTP requests to the deployed Hugging Face Space URL.
630
+ """)
631
+
632
+ # --- Main Application ---
633
+ if __name__ == "__main__":
634
+ # Check if running in Hugging Face Spaces
635
+ import os
636
+ port = int(os.environ.get("PORT", 7860))
637
 
638
+ logger.info("Starting Earthquake Data MCP Server...")
639
+ logger.info(f"Server will be available at: http://0.0.0.0:{port}")
 
 
 
640
 
641
+ app.launch(
642
+ server_name="0.0.0.0",
643
+ server_port=port,
644
+ share=True,
645
+ show_api=True
646
+ )