File size: 7,230 Bytes
ab8532e
 
b2ff92e
7d1cee1
b2ff92e
038aba5
b2ff92e
7d1cee1
 
 
 
 
 
 
 
b2ff92e
 
4cab5dd
 
 
 
 
 
 
 
ab8532e
 
 
 
 
 
 
 
 
 
 
90209fb
ab8532e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90209fb
 
ab8532e
 
 
 
f360e0e
ab8532e
 
 
 
 
 
 
7d1cee1
038aba5
 
7d1cee1
038aba5
7d1cee1
b2ff92e
f360e0e
7d1cee1
ab8532e
b2ff92e
7d1cee1
 
038aba5
 
7d1cee1
 
 
 
 
 
b2ff92e
96d425d
7d1cee1
b2ff92e
 
038aba5
7d1cee1
038aba5
 
7d1cee1
 
 
 
 
 
b2ff92e
038aba5
7d1cee1
038aba5
 
7d1cee1
038aba5
 
 
 
 
 
 
 
7d1cee1
038aba5
7d1cee1
038aba5
 
 
 
 
bb8d9e3
ab8532e
 
 
038aba5
ab8532e
 
f360e0e
ab8532e
 
 
038aba5
 
 
4cab5dd
038aba5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b2ff92e
5c2808f
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
from dataclasses import dataclass
from typing import Optional, List
import gradio as gr
import logging
from camptocamp_api import CamptocampAPI
from typing import Optional

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s"
)
logger = logging.getLogger("CamptocampApp")

# Instantiate API client
c2c = CamptocampAPI(language="en")

ACTIVITIES = [
    "hiking", "snowshoeing", "skitouring", "snow_ice_mixed",
    "rock_climbing", "ice_climbing", "mountaineering", "via_ferrata",
    "paragliding", "bouldering", "multi_pitch",
    "bike", "mountain_bike", "trail_running", "alpine_climbing"
]


@dataclass
class SimplifiedOuting:
    title: Optional[str]
    condition_rating: Optional[str]
    date_start: Optional[str]
    date_end: Optional[str]
    elevation_max: Optional[int]
    global_rating: Optional[str]
    equipment_rating: Optional[str]
    rock_free_rating: Optional[str]
    area_titles: List[str]
    link: str


def simplify_outings_response(
    response: dict,
    elevation_max_threshold: Optional[int] = None
) -> List[SimplifiedOuting]:
    results = []
    for doc in response.get("documents", []):
        elevation = doc.get("elevation_max")
        if elevation is None:
            continue
        elif elevation_max_threshold is not None and elevation is not None:
            if elevation < elevation_max_threshold:
                continue  # filter it out

        title = None
        if doc.get("locales"):
            title = doc["locales"][0].get("title")

        area_titles = []
        for area in doc.get("areas", []):
            for loc in area.get("locales", []):
                t = loc.get("title")
                if t:
                    area_titles.append(t)
                    break

        results.append(SimplifiedOuting(
            title=title,
            condition_rating=doc.get("condition_rating"),
            date_start=doc.get("date_start"),
            date_end=doc.get("date_end"),
            elevation_max=elevation,
            global_rating=doc.get("global_rating"),
            equipment_rating=doc.get("equipment_rating"),
            rock_free_rating=doc.get("rock_free_rating"),
            area_titles=area_titles,
            link=f"https://www.camptocamp.org/outings/{doc.get('document_id')}"
        ))
    return results


def get_outings_by_location(
    location: str,
    start_date: Optional[str] = None,
    end_date: Optional[str] = None,
    activity: Optional[str] = None,
    limit: int = 10,
    elevation_max_threshold: Optional[int] = 4000
) -> List[SimplifiedOuting]:
    logger.info(f"[Outings] Resolving location: {location}")
    bbox = c2c.get_bbox_from_location(location)
    if not bbox:
        logger.warning(f"No bounding box found for: {location}")
        return {"error": f"Could not resolve bounding box for location: {location}"}
    logger.info(f"BBox for '{location}': {bbox}")
    date_range = (start_date, end_date) if start_date and end_date else None
    result = c2c.get_outings(bbox, date_range, activity, limit)
    logger.info(f"Returned {len(result.get('documents', []))} outings.")
    return simplify_outings_response(result, elevation_max_threshold=elevation_max_threshold)

def search_routes_by_location(location: str, activity: str, limit: int = 10) -> dict:
    logger.info(f"[Routes] Resolving location: {location}")
    bbox = c2c.get_bbox_from_location(location)
    if not bbox:
        logger.warning(f"No bounding box found for: {location}")
        return {"error": f"Could not resolve bounding box for location: {location}"}
    logger.info(f"BBox for '{location}': {bbox}")
    result = c2c.search_routes_by_activity(bbox, activity, limit)
    logger.info(f"Returned {len(result.get('documents', []))} routes.")
    return result

def get_route_details(route_id: int) -> dict:
    logger.info(f"[Details] Fetching route ID: {route_id}")
    return c2c.get_route_details(route_id)

def search_waypoints_by_location(location: str, limit: int = 10) -> dict:
    logger.info(f"[Waypoints] Resolving location: {location}")
    bbox = c2c.get_bbox_from_location(location)
    if not bbox:
        logger.warning(f"No bounding box found for: {location}")
        return {"error": f"Could not resolve bounding box for location: {location}"}
    logger.info(f"BBox for '{location}': {bbox}")
    result = c2c.search_waypoints(bbox, limit)
    logger.info(f"Returned {len(result.get('documents', []))} waypoints.")
    return result

def lookup_bbox_from_location(location_name: str) -> Optional[dict]:
    logger.info(f"[Lookup] Resolving location: {location_name}")
    bbox = c2c.get_bbox_from_location(location_name)
    if not bbox:
        logger.warning(f"No bounding box found for: {location_name}")
        return {"error": f"No bounding box found for: {location_name}"}
    return {
        "west": bbox[0],
        "south": bbox[1],
        "east": bbox[2],
        "north": bbox[3]
    }

# Gradio UI
with gr.Blocks(title="Camptocamp MCP Server") as demo:
    gr.Markdown("# πŸ”οΈ Camptocamp API MCP")

    with gr.Tab("πŸ“ Recent Outings"):
        loc = gr.Textbox(label="Location (e.g. Chamonix, La Grave)")
        start = gr.Textbox(label="Start Date (YYYY-MM-DD)")
        end = gr.Textbox(label="End Date (YYYY-MM-DD)")
        act = gr.Dropdown(label="Activity", choices=ACTIVITIES, value="alpine_climbing")
        limit = gr.Number(label="Result Limit", value=30)
        elev_slider = gr.Slider(label="Minimum maximal elevation (m)", minimum=0, maximum=4000, value=4000, step=100)

        out = gr.JSON()

        gr.Button("Get Outings").click(
            get_outings_by_location,
            inputs=[loc, start, end, act, limit, elev_slider],
            outputs=out
        )

    with gr.Tab("πŸ§— Search Routes"):
        rloc = gr.Textbox(label="Location (e.g. Alps)")
        ract = gr.Dropdown(label="Activity", choices=ACTIVITIES, value="alpine_climbing")
        rlim = gr.Number(label="Result Limit", value=5)
        rout = gr.JSON()
        gr.Button("Search Routes").click(search_routes_by_location,
                                         inputs=[rloc, ract, rlim],
                                         outputs=rout)

    with gr.Tab("πŸ“„ Route Details"):
        rid = gr.Number(label="Route ID")
        rdet = gr.JSON()
        gr.Button("Get Route Details").click(get_route_details,
                                             inputs=[rid],
                                             outputs=rdet)

    with gr.Tab("β›° Waypoints"):
        wloc = gr.Textbox(label="Location (e.g. Mont Blanc)")
        wlim = gr.Number(label="Result Limit", value=5)
        wout = gr.JSON()
        gr.Button("Get Waypoints").click(search_waypoints_by_location,
                                         inputs=[wloc, wlim],
                                         outputs=wout)

    with gr.Tab("🌍 Location β†’ BBox"):
        lstr = gr.Textbox(label="Location Name")
        lout = gr.JSON()
        gr.Button("Lookup BBox").click(lookup_bbox_from_location,
                                       inputs=[lstr],
                                       outputs=lout)

demo.launch(mcp_server=True)