snowsure-mcp-server 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +165 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +891 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# SnowSure MCP Server — ski resort snow for Cursor, Claude & AI agents
|
|
2
|
+
|
|
3
|
+
**`npx snowsure-mcp-server`** — [Model Context Protocol](https://modelcontextprotocol.io/) server for **real-time powder rankings**, **14-day multi-model snow forecasts**, and **500+ ski resorts** via [SnowSure](https://www.snowsure.ai). Use it when building agents that answer: *where is the best snow*, *fresh powder in Japan/the Alps*, *compare two resorts*, or *trip planning by conditions*.
|
|
4
|
+
|
|
5
|
+
- Docs: [snowsure.ai/developers](https://www.snowsure.ai/developers) · [llms.txt](https://www.snowsure.ai/llms.txt) · [OpenAPI](https://www.snowsure.ai/openapi.json)
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **11 AI Tools** for querying snow conditions, forecasts, and resort information
|
|
10
|
+
- **4 Resources** for direct data access
|
|
11
|
+
- **500+ ski resorts** worldwide
|
|
12
|
+
- **7 weather models** for forecast comparison
|
|
13
|
+
- **Frequent updates** (weather pipeline ~15 min; see [methodology](https://www.snowsure.ai/methodology))
|
|
14
|
+
|
|
15
|
+
## Available Tools
|
|
16
|
+
|
|
17
|
+
| Tool | Description |
|
|
18
|
+
|------|-------------|
|
|
19
|
+
| `get_snow_report` | Global snow rankings by score, forecast, or recent snowfall |
|
|
20
|
+
| `get_resort` | Comprehensive resort details including AI analysis |
|
|
21
|
+
| `search_resorts` | Find resorts by name, country, or region |
|
|
22
|
+
| `find_best_powder` | Resorts with freshest powder (24h snowfall) |
|
|
23
|
+
| `compare_forecasts` | Multi-model forecast comparison with confidence |
|
|
24
|
+
| `get_weather_forecast` | Day-by-day weather for up to 14 days |
|
|
25
|
+
| `find_resorts_by_criteria` | Advanced filtering (depth, elevation, runs, score) |
|
|
26
|
+
| `get_snow_history` | Historical data and season comparisons |
|
|
27
|
+
| `plan_ski_trip` | AI-powered trip recommendations |
|
|
28
|
+
| `get_webcam_status` | Live webcam links for a resort |
|
|
29
|
+
| `get_regional_summary` | Regional statistics and top resorts |
|
|
30
|
+
|
|
31
|
+
## Available Resources
|
|
32
|
+
|
|
33
|
+
| URI | Description |
|
|
34
|
+
|-----|-------------|
|
|
35
|
+
| `snowsure://snow-report` | Current global snow report (top 50) |
|
|
36
|
+
| `snowsure://resorts` | All 500+ resorts with conditions |
|
|
37
|
+
| `snowsure://regions` | Region/country breakdown |
|
|
38
|
+
| `snowsure://api-docs` | API documentation |
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
### For Claude Desktop
|
|
43
|
+
|
|
44
|
+
Add to your `claude_desktop_config.json`:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"snowsure": {
|
|
50
|
+
"command": "npx",
|
|
51
|
+
"args": ["-y", "snowsure-mcp-server"]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### For Cursor
|
|
58
|
+
|
|
59
|
+
Add to your MCP settings:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"snowsure": {
|
|
64
|
+
"command": "npx",
|
|
65
|
+
"args": ["-y", "snowsure-mcp-server"]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Manual Installation
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Clone and build
|
|
74
|
+
git clone https://github.com/mikeslone/snowsure-web.git
|
|
75
|
+
cd snowsure-web/mcp-server
|
|
76
|
+
npm install
|
|
77
|
+
npm run build
|
|
78
|
+
|
|
79
|
+
# Run
|
|
80
|
+
node dist/index.js
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Example Queries
|
|
84
|
+
|
|
85
|
+
Once connected, you can ask your AI assistant:
|
|
86
|
+
|
|
87
|
+
- "What are the best ski conditions right now?"
|
|
88
|
+
- "Tell me about Niseko's snow conditions"
|
|
89
|
+
- "Find me powder in Japan"
|
|
90
|
+
- "Compare forecasts for Matterhorn"
|
|
91
|
+
- "Plan a ski trip to the Alps in February"
|
|
92
|
+
- "Which resorts have the deepest snow base?"
|
|
93
|
+
- "What's the weather forecast for Jackson Hole this week?"
|
|
94
|
+
|
|
95
|
+
## Tool Usage Examples
|
|
96
|
+
|
|
97
|
+
### Get Snow Report
|
|
98
|
+
```
|
|
99
|
+
Query: "Show me the top 5 resorts in Europe right now"
|
|
100
|
+
Tool: get_snow_report
|
|
101
|
+
Args: { "region": "europe", "limit": 5, "sort": "snowsure" }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Find Best Powder
|
|
105
|
+
```
|
|
106
|
+
Query: "Where is it dumping in North America?"
|
|
107
|
+
Tool: find_best_powder
|
|
108
|
+
Args: { "region": "north-america", "minSnowfall": 15 }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Compare Forecasts
|
|
112
|
+
```
|
|
113
|
+
Query: "How reliable is the Whistler forecast?"
|
|
114
|
+
Tool: compare_forecasts
|
|
115
|
+
Args: { "slug": "whistler-blackcomb" }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Plan Trip
|
|
119
|
+
```
|
|
120
|
+
Query: "Recommend a ski trip for an advanced skier in Japan"
|
|
121
|
+
Tool: plan_ski_trip
|
|
122
|
+
Args: { "region": "asia", "level": "advanced" }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## API Reference
|
|
126
|
+
|
|
127
|
+
The MCP server connects to the SnowSure API at `https://lux.ski/api/v1`.
|
|
128
|
+
|
|
129
|
+
### Endpoints Used
|
|
130
|
+
- `GET /resorts` - List all resorts
|
|
131
|
+
- `GET /resorts/{slug}` - Get resort details
|
|
132
|
+
- `GET /snow-report` - Get ranked snow report
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Install dependencies
|
|
138
|
+
npm install
|
|
139
|
+
|
|
140
|
+
# Run in development mode
|
|
141
|
+
npm run dev
|
|
142
|
+
|
|
143
|
+
# Build for production
|
|
144
|
+
npm run build
|
|
145
|
+
|
|
146
|
+
# Watch mode
|
|
147
|
+
npm run watch
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Data Sources
|
|
151
|
+
|
|
152
|
+
- **Weather Models:** ECMWF, GFS, GEM, JMA, ICON, Météo-France, Met Norway
|
|
153
|
+
- **Conditions:** Resort weather pipeline ~15 min cadence (production)
|
|
154
|
+
- **Historical:** 30 years of snowfall records
|
|
155
|
+
- **SnowSure Score:** AI-powered rating combining depth, recent snow, forecast, and historical reliability
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
160
|
+
|
|
161
|
+
## Support
|
|
162
|
+
|
|
163
|
+
- Website: [snowsure.ai](https://www.snowsure.ai)
|
|
164
|
+
- API Docs: [snowsure.ai/developers](https://www.snowsure.ai/developers)
|
|
165
|
+
- Email: support@snowsure.ai
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SnowSure MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Provides AI agents with comprehensive access to real-time snow conditions,
|
|
6
|
+
* forecasts, historical data, and resort information from SnowSure.ai
|
|
7
|
+
*
|
|
8
|
+
* Tools:
|
|
9
|
+
* - get_snow_report: Global snow rankings
|
|
10
|
+
* - get_resort: Detailed resort info
|
|
11
|
+
* - search_resorts: Find resorts by name/country
|
|
12
|
+
* - find_best_powder: Fresh snow rankings
|
|
13
|
+
* - compare_forecasts: Multi-model comparison
|
|
14
|
+
* - get_weather_forecast: Daily forecast data
|
|
15
|
+
* - find_resorts_by_criteria: Advanced filtering
|
|
16
|
+
* - get_snow_history: Historical snowfall data
|
|
17
|
+
* - plan_ski_trip: Trip recommendations
|
|
18
|
+
* - get_webcam_status: Live webcam info
|
|
19
|
+
*
|
|
20
|
+
* Resources:
|
|
21
|
+
* - snowsure://snow-report: Live snow report
|
|
22
|
+
* - snowsure://resorts: All resorts list
|
|
23
|
+
* - snowsure://regions: Available regions
|
|
24
|
+
*/
|
|
25
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SnowSure MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Provides AI agents with comprehensive access to real-time snow conditions,
|
|
6
|
+
* forecasts, historical data, and resort information from SnowSure.ai
|
|
7
|
+
*
|
|
8
|
+
* Tools:
|
|
9
|
+
* - get_snow_report: Global snow rankings
|
|
10
|
+
* - get_resort: Detailed resort info
|
|
11
|
+
* - search_resorts: Find resorts by name/country
|
|
12
|
+
* - find_best_powder: Fresh snow rankings
|
|
13
|
+
* - compare_forecasts: Multi-model comparison
|
|
14
|
+
* - get_weather_forecast: Daily forecast data
|
|
15
|
+
* - find_resorts_by_criteria: Advanced filtering
|
|
16
|
+
* - get_snow_history: Historical snowfall data
|
|
17
|
+
* - plan_ski_trip: Trip recommendations
|
|
18
|
+
* - get_webcam_status: Live webcam info
|
|
19
|
+
*
|
|
20
|
+
* Resources:
|
|
21
|
+
* - snowsure://snow-report: Live snow report
|
|
22
|
+
* - snowsure://resorts: All resorts list
|
|
23
|
+
* - snowsure://regions: Available regions
|
|
24
|
+
*/
|
|
25
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
26
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
27
|
+
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
28
|
+
const API_BASE = 'https://www.snowsure.ai/api/v1';
|
|
29
|
+
// Helper to fetch from API with error handling
|
|
30
|
+
async function fetchAPI(endpoint) {
|
|
31
|
+
try {
|
|
32
|
+
const res = await fetch(`${API_BASE}${endpoint}`, {
|
|
33
|
+
headers: {
|
|
34
|
+
'User-Agent': 'SnowSure-MCP/3.0.1',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
throw new Error(`API error: ${res.status} ${res.statusText}`);
|
|
39
|
+
}
|
|
40
|
+
return res.json();
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
throw new Error(`Failed to fetch ${endpoint}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Format date helper
|
|
47
|
+
function formatDate(dateStr) {
|
|
48
|
+
if (!dateStr)
|
|
49
|
+
return 'Unknown';
|
|
50
|
+
const date = new Date(dateStr);
|
|
51
|
+
return date.toLocaleDateString('en-US', {
|
|
52
|
+
weekday: 'short',
|
|
53
|
+
month: 'short',
|
|
54
|
+
day: 'numeric'
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// Create the MCP server
|
|
58
|
+
const server = new Server({
|
|
59
|
+
name: 'snowsure-mcp',
|
|
60
|
+
version: '3.0.1',
|
|
61
|
+
}, {
|
|
62
|
+
capabilities: {
|
|
63
|
+
resources: {},
|
|
64
|
+
tools: {},
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
// List available tools - comprehensive set for AI agents
|
|
68
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
69
|
+
tools: [
|
|
70
|
+
// === CORE TOOLS ===
|
|
71
|
+
{
|
|
72
|
+
name: 'get_snow_report',
|
|
73
|
+
description: 'Get the global snow report with top-ranked resorts by snow conditions. Returns resorts sorted by SnowSure score, forecast, or recent snowfall. Use this for "where has the best snow?" or "top ski resorts right now" queries.',
|
|
74
|
+
inputSchema: {
|
|
75
|
+
type: 'object',
|
|
76
|
+
properties: {
|
|
77
|
+
sort: {
|
|
78
|
+
type: 'string',
|
|
79
|
+
enum: ['snowsure', 'forecast', 'recent', 'depth'],
|
|
80
|
+
description: 'Sort order: snowsure (AI rating), forecast (14-day snow), recent (24h snowfall), depth (current base)',
|
|
81
|
+
},
|
|
82
|
+
limit: {
|
|
83
|
+
type: 'number',
|
|
84
|
+
description: 'Number of resorts to return (default: 10, max: 50)',
|
|
85
|
+
},
|
|
86
|
+
region: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
enum: ['europe', 'north-america', 'asia', 'oceania', 'south-america'],
|
|
89
|
+
description: 'Filter by region',
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'get_resort',
|
|
96
|
+
description: 'Get comprehensive information about a specific ski resort including current conditions, multi-model forecasts, historical data, webcams, and AI analysis. Use for "tell me about [resort]" or "[resort] conditions" queries.',
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: {
|
|
100
|
+
slug: {
|
|
101
|
+
type: 'string',
|
|
102
|
+
description: 'Resort slug identifier (e.g., "niseko-hanazono-resort", "matterhorn-ski-paradise", "jackson-hole-mountain-resort")',
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
required: ['slug'],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'search_resorts',
|
|
110
|
+
description: 'Search for ski resorts by name, country, or region. Returns matching resorts with basic conditions. Use for "find resorts in [location]" or "search [name]" queries.',
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: {
|
|
114
|
+
query: {
|
|
115
|
+
type: 'string',
|
|
116
|
+
description: 'Search query - can be resort name, country, or partial match',
|
|
117
|
+
},
|
|
118
|
+
country: {
|
|
119
|
+
type: 'string',
|
|
120
|
+
description: 'Filter by exact country name (e.g., "Japan", "Switzerland", "United States")',
|
|
121
|
+
},
|
|
122
|
+
limit: {
|
|
123
|
+
type: 'number',
|
|
124
|
+
description: 'Maximum results to return (default: 20)',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'find_best_powder',
|
|
131
|
+
description: 'Find resorts with the freshest powder snow right now. Returns resorts sorted by 24-hour snowfall. Use for "where is it snowing?" or "fresh powder" queries.',
|
|
132
|
+
inputSchema: {
|
|
133
|
+
type: 'object',
|
|
134
|
+
properties: {
|
|
135
|
+
minSnowfall: {
|
|
136
|
+
type: 'number',
|
|
137
|
+
description: 'Minimum 24h snowfall in cm to include (default: 5)',
|
|
138
|
+
},
|
|
139
|
+
region: {
|
|
140
|
+
type: 'string',
|
|
141
|
+
enum: ['europe', 'north-america', 'asia', 'oceania'],
|
|
142
|
+
description: 'Filter by region',
|
|
143
|
+
},
|
|
144
|
+
limit: {
|
|
145
|
+
type: 'number',
|
|
146
|
+
description: 'Number of results (default: 10)',
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: 'compare_forecasts',
|
|
153
|
+
description: 'Compare 14-day snow forecasts across 7 weather models (ECMWF, GFS, GEM, JMA, ICON, Météo-France, Met Norway) for a resort. Shows model agreement and uncertainty. Use for forecast reliability queries.',
|
|
154
|
+
inputSchema: {
|
|
155
|
+
type: 'object',
|
|
156
|
+
properties: {
|
|
157
|
+
slug: {
|
|
158
|
+
type: 'string',
|
|
159
|
+
description: 'Resort slug to compare forecasts for',
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
required: ['slug'],
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
// === ADVANCED TOOLS ===
|
|
166
|
+
{
|
|
167
|
+
name: 'get_weather_forecast',
|
|
168
|
+
description: 'Get detailed day-by-day weather forecast for a resort including temperature, snowfall, wind, and conditions for each of the next 14 days.',
|
|
169
|
+
inputSchema: {
|
|
170
|
+
type: 'object',
|
|
171
|
+
properties: {
|
|
172
|
+
slug: {
|
|
173
|
+
type: 'string',
|
|
174
|
+
description: 'Resort slug',
|
|
175
|
+
},
|
|
176
|
+
days: {
|
|
177
|
+
type: 'number',
|
|
178
|
+
description: 'Number of days to forecast (default: 7, max: 14)',
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
required: ['slug'],
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: 'find_resorts_by_criteria',
|
|
186
|
+
description: 'Find resorts matching specific criteria like minimum snow depth, elevation range, number of runs, or SnowSure rating. Advanced filtering for trip planning.',
|
|
187
|
+
inputSchema: {
|
|
188
|
+
type: 'object',
|
|
189
|
+
properties: {
|
|
190
|
+
minScore: {
|
|
191
|
+
type: 'number',
|
|
192
|
+
description: 'Minimum SnowSure score (0-100)',
|
|
193
|
+
},
|
|
194
|
+
minDepth: {
|
|
195
|
+
type: 'number',
|
|
196
|
+
description: 'Minimum snow depth in cm',
|
|
197
|
+
},
|
|
198
|
+
minElevation: {
|
|
199
|
+
type: 'number',
|
|
200
|
+
description: 'Minimum summit elevation in meters',
|
|
201
|
+
},
|
|
202
|
+
minRuns: {
|
|
203
|
+
type: 'number',
|
|
204
|
+
description: 'Minimum number of ski runs',
|
|
205
|
+
},
|
|
206
|
+
country: {
|
|
207
|
+
type: 'string',
|
|
208
|
+
description: 'Filter by country',
|
|
209
|
+
},
|
|
210
|
+
region: {
|
|
211
|
+
type: 'string',
|
|
212
|
+
enum: ['europe', 'north-america', 'asia'],
|
|
213
|
+
description: 'Filter by region',
|
|
214
|
+
},
|
|
215
|
+
limit: {
|
|
216
|
+
type: 'number',
|
|
217
|
+
description: 'Max results (default: 20)',
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: 'get_snow_history',
|
|
224
|
+
description: 'Get historical snowfall data for a resort including season totals, comparison to 5-year and 30-year averages, and best months to visit.',
|
|
225
|
+
inputSchema: {
|
|
226
|
+
type: 'object',
|
|
227
|
+
properties: {
|
|
228
|
+
slug: {
|
|
229
|
+
type: 'string',
|
|
230
|
+
description: 'Resort slug',
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
required: ['slug'],
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'plan_ski_trip',
|
|
238
|
+
description: 'Get AI-powered ski trip recommendations based on dates, preferences, and conditions. Suggests best resorts for a given time period.',
|
|
239
|
+
inputSchema: {
|
|
240
|
+
type: 'object',
|
|
241
|
+
properties: {
|
|
242
|
+
region: {
|
|
243
|
+
type: 'string',
|
|
244
|
+
enum: ['europe', 'north-america', 'asia', 'any'],
|
|
245
|
+
description: 'Preferred region (default: any)',
|
|
246
|
+
},
|
|
247
|
+
dates: {
|
|
248
|
+
type: 'string',
|
|
249
|
+
description: 'Travel dates or month (e.g., "February", "next week", "Jan 15-22")',
|
|
250
|
+
},
|
|
251
|
+
level: {
|
|
252
|
+
type: 'string',
|
|
253
|
+
enum: ['beginner', 'intermediate', 'advanced', 'expert'],
|
|
254
|
+
description: 'Skier ability level',
|
|
255
|
+
},
|
|
256
|
+
priorities: {
|
|
257
|
+
type: 'array',
|
|
258
|
+
items: {
|
|
259
|
+
type: 'string',
|
|
260
|
+
enum: ['powder', 'groomed', 'terrain-variety', 'short-lift-lines', 'apres-ski', 'family-friendly', 'budget'],
|
|
261
|
+
},
|
|
262
|
+
description: 'Trip priorities',
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: 'get_webcam_status',
|
|
269
|
+
description: 'Get live webcam information and links for a resort to see current conditions visually.',
|
|
270
|
+
inputSchema: {
|
|
271
|
+
type: 'object',
|
|
272
|
+
properties: {
|
|
273
|
+
slug: {
|
|
274
|
+
type: 'string',
|
|
275
|
+
description: 'Resort slug',
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
required: ['slug'],
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: 'get_regional_summary',
|
|
283
|
+
description: 'Get a summary of snow conditions across an entire region or country with statistics and top resorts.',
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: 'object',
|
|
286
|
+
properties: {
|
|
287
|
+
region: {
|
|
288
|
+
type: 'string',
|
|
289
|
+
enum: ['europe', 'north-america', 'asia', 'alps', 'rockies', 'japan'],
|
|
290
|
+
description: 'Region to summarize',
|
|
291
|
+
},
|
|
292
|
+
country: {
|
|
293
|
+
type: 'string',
|
|
294
|
+
description: 'Specific country (alternative to region)',
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
}));
|
|
301
|
+
// Handle tool calls
|
|
302
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
303
|
+
const { name, arguments: args } = request.params;
|
|
304
|
+
try {
|
|
305
|
+
switch (name) {
|
|
306
|
+
// === CORE TOOLS ===
|
|
307
|
+
case 'get_snow_report': {
|
|
308
|
+
const params = new URLSearchParams();
|
|
309
|
+
if (args?.sort)
|
|
310
|
+
params.set('sort', args.sort);
|
|
311
|
+
if (args?.limit)
|
|
312
|
+
params.set('limit', String(args.limit));
|
|
313
|
+
if (args?.region)
|
|
314
|
+
params.set('region', args.region);
|
|
315
|
+
const data = await fetchAPI(`/snow-report?${params}`);
|
|
316
|
+
const resorts = data.data?.resorts || data.data || [];
|
|
317
|
+
const summary = `# 🏔️ Global Snow Report\n\n` +
|
|
318
|
+
`**Updated:** ${new Date().toLocaleString()}\n\n` +
|
|
319
|
+
`## Top Resorts\n\n` +
|
|
320
|
+
resorts.slice(0, args?.limit || 10).map((r, i) => `${i + 1}. **${r.name}** (${r.country})\n` +
|
|
321
|
+
` - SnowSure Score: ${r.snowSure?.score || 'N/A'}/100 (${r.snowSure?.rating || 'N/A'})\n` +
|
|
322
|
+
` - Snow Depth: ${r.conditions?.snowDepthCm || 'N/A'}cm\n` +
|
|
323
|
+
` - 24h Snowfall: ${r.conditions?.snowfall24hCm || 0}cm\n` +
|
|
324
|
+
` - 14-Day Forecast: ${Math.round(r.conditions?.forecast14dCm || 0)}cm\n`).join('\n');
|
|
325
|
+
return {
|
|
326
|
+
content: [{ type: 'text', text: summary }],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
case 'get_resort': {
|
|
330
|
+
const data = await fetchAPI(`/resorts/${args?.slug}`);
|
|
331
|
+
const resort = data.data;
|
|
332
|
+
if (!resort) {
|
|
333
|
+
return {
|
|
334
|
+
content: [{ type: 'text', text: `Resort "${args?.slug}" not found. Try searching for resorts first.` }],
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const summary = `# ${resort.name}\n\n` +
|
|
338
|
+
`**Location:** ${resort.country}${resort.region ? ` (${resort.region})` : ''}\n` +
|
|
339
|
+
`**Website:** ${resort.links?.website || 'N/A'}\n\n` +
|
|
340
|
+
`## SnowSure™ Score: ${resort.snowSure?.score || 'N/A'}/100\n` +
|
|
341
|
+
`**Rating:** ${resort.snowSure?.rating || 'N/A'}\n` +
|
|
342
|
+
`**Trend:** ${resort.snowSure?.trend || 'stable'}\n\n` +
|
|
343
|
+
`${resort.snowSure?.analysis || resort.snowSure?.tagline || ''}\n\n` +
|
|
344
|
+
`### Score Breakdown\n` +
|
|
345
|
+
`- Depth: ${resort.snowSure?.breakdown?.depth || 'N/A'}/25\n` +
|
|
346
|
+
`- Recent Snow: ${resort.snowSure?.breakdown?.recent || 'N/A'}/25\n` +
|
|
347
|
+
`- Forecast: ${resort.snowSure?.breakdown?.forecast || 'N/A'}/25\n` +
|
|
348
|
+
`- Historic: ${resort.snowSure?.breakdown?.historic || 'N/A'}/25\n\n` +
|
|
349
|
+
`## Current Conditions\n` +
|
|
350
|
+
`- **Temperature:** ${resort.currentConditions?.temperature?.celsius || 'N/A'}°C\n` +
|
|
351
|
+
`- **Conditions:** ${resort.currentConditions?.conditions || 'N/A'}\n` +
|
|
352
|
+
`- **Wind:** ${resort.currentConditions?.wind?.speed || 'N/A'} km/h\n` +
|
|
353
|
+
`- **Snow Depth:** ${resort.snow?.depth?.cm || 'N/A'}cm\n` +
|
|
354
|
+
`- **Last 24h:** ${resort.snow?.last24h?.cm || 0}cm\n` +
|
|
355
|
+
`- **Last 7 Days:** ${resort.snow?.last7d?.cm || 0}cm\n` +
|
|
356
|
+
`- **Season Total:** ${resort.snow?.seasonTotal?.cm || 'N/A'}cm\n` +
|
|
357
|
+
`- **Last Updated:** ${formatDate(resort.currentConditions?.lastUpdated)}\n\n` +
|
|
358
|
+
`## 14-Day Forecast (Multi-Model)\n` +
|
|
359
|
+
`- **SnowSure Average:** ${resort.forecast?.total14d?.average || 'N/A'}cm\n` +
|
|
360
|
+
`- **ECMWF:** ${resort.forecast?.total14d?.ecmwf || 'N/A'}cm\n` +
|
|
361
|
+
`- **GFS:** ${resort.forecast?.total14d?.gfs || 'N/A'}cm\n` +
|
|
362
|
+
`- **GEM:** ${resort.forecast?.total14d?.gem || 'N/A'}cm\n` +
|
|
363
|
+
`- **JMA:** ${resort.forecast?.total14d?.jma || 'N/A'}cm\n` +
|
|
364
|
+
`- **ICON:** ${resort.forecast?.total14d?.icon || 'N/A'}cm\n\n` +
|
|
365
|
+
`## Resort Info\n` +
|
|
366
|
+
`- **Summit:** ${resort.elevation?.summit || 'N/A'}m\n` +
|
|
367
|
+
`- **Base:** ${resort.elevation?.base || 'N/A'}m\n` +
|
|
368
|
+
`- **Vertical Drop:** ${resort.elevation?.vertical || 'N/A'}m\n` +
|
|
369
|
+
`- **Runs:** ${resort.stats?.runs || 'N/A'}\n` +
|
|
370
|
+
`- **Lifts:** ${resort.stats?.lifts || 'N/A'}\n` +
|
|
371
|
+
`- **Skiable Area:** ${resort.stats?.skiableAcres || 'N/A'} acres\n` +
|
|
372
|
+
`- **Annual Snowfall:** ${resort.stats?.annualSnowfall || 'N/A'}\n\n` +
|
|
373
|
+
`${resort.description ? `## About\n${resort.description}\n\n` : ''}` +
|
|
374
|
+
`${resort.topApresSki ? `## Top Après-Ski\n${resort.topApresSki}\n\n` : ''}` +
|
|
375
|
+
`---\n` +
|
|
376
|
+
`*View more at: https://snowsure.ai/resorts/${resort.slug}*`;
|
|
377
|
+
return {
|
|
378
|
+
content: [{ type: 'text', text: summary }],
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
case 'search_resorts': {
|
|
382
|
+
const data = await fetchAPI('/resorts?limit=200');
|
|
383
|
+
let results = data.data || [];
|
|
384
|
+
if (args?.query) {
|
|
385
|
+
const query = args.query.toLowerCase();
|
|
386
|
+
results = results.filter((r) => r.name?.toLowerCase().includes(query) ||
|
|
387
|
+
r.country?.toLowerCase().includes(query) ||
|
|
388
|
+
r.region?.toLowerCase().includes(query) ||
|
|
389
|
+
r.slug?.toLowerCase().includes(query));
|
|
390
|
+
}
|
|
391
|
+
if (args?.country) {
|
|
392
|
+
const country = args.country.toLowerCase();
|
|
393
|
+
results = results.filter((r) => r.country?.toLowerCase() === country);
|
|
394
|
+
}
|
|
395
|
+
const limit = args?.limit || 20;
|
|
396
|
+
const formatted = results.slice(0, limit).map((r) => `- **${r.name}** (${r.country})\n` +
|
|
397
|
+
` Slug: \`${r.slug}\` | Score: ${r.snowSure?.score || 'N/A'} | Depth: ${r.conditions?.snowDepthCm || 'N/A'}cm`).join('\n');
|
|
398
|
+
return {
|
|
399
|
+
content: [
|
|
400
|
+
{
|
|
401
|
+
type: 'text',
|
|
402
|
+
text: `# Search Results\n\nFound **${results.length}** resorts${args?.query ? ` matching "${args.query}"` : ''}${args?.country ? ` in ${args.country}` : ''}:\n\n${formatted}\n\n*Use the slug value with get_resort for detailed info.*`,
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
case 'find_best_powder': {
|
|
408
|
+
const data = await fetchAPI('/snow-report?sort=recent&limit=50');
|
|
409
|
+
let results = data.data?.resorts || data.data || [];
|
|
410
|
+
const minSnow = args?.minSnowfall || 0;
|
|
411
|
+
results = results.filter((r) => (r.conditions?.snowfall24hCm || 0) >= minSnow);
|
|
412
|
+
if (args?.region) {
|
|
413
|
+
const regionMap = {
|
|
414
|
+
'europe': ['Switzerland', 'France', 'Austria', 'Italy', 'Germany', 'Norway', 'Sweden'],
|
|
415
|
+
'north-america': ['United States', 'Canada'],
|
|
416
|
+
'asia': ['Japan'],
|
|
417
|
+
'oceania': ['New Zealand', 'Australia'],
|
|
418
|
+
};
|
|
419
|
+
const countries = regionMap[args.region] || [];
|
|
420
|
+
if (countries.length > 0) {
|
|
421
|
+
results = results.filter((r) => countries.includes(r.country));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const limit = args?.limit || 10;
|
|
425
|
+
const formatted = results.slice(0, limit).map((r, i) => `${i + 1}. 🏔️ **${r.name}** (${r.country})\n` +
|
|
426
|
+
` ❄️ **+${r.conditions?.snowfall24hCm || 0}cm** in last 24 hours\n` +
|
|
427
|
+
` 📏 Total Depth: ${r.conditions?.snowDepthCm || 'N/A'}cm\n` +
|
|
428
|
+
` 🎯 SnowSure Score: ${r.snowSure?.score || 'N/A'}/100`).join('\n\n');
|
|
429
|
+
return {
|
|
430
|
+
content: [
|
|
431
|
+
{
|
|
432
|
+
type: 'text',
|
|
433
|
+
text: `# 🎿 Fresh Powder Report\n\n${formatted || 'No resorts with fresh powder found matching your criteria.'}\n\n*Data updated: ${new Date().toLocaleString()}*`,
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
case 'compare_forecasts': {
|
|
439
|
+
const data = await fetchAPI(`/resorts/${args?.slug}`);
|
|
440
|
+
const resort = data.data;
|
|
441
|
+
if (!resort?.forecast?.total14d) {
|
|
442
|
+
return {
|
|
443
|
+
content: [{ type: 'text', text: 'Forecast data not available for this resort.' }],
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const f = resort.forecast.total14d;
|
|
447
|
+
const models = [
|
|
448
|
+
{ name: 'ECMWF', value: f.ecmwf, desc: 'European model (most accurate)' },
|
|
449
|
+
{ name: 'GFS', value: f.gfs, desc: 'US Global Forecast System' },
|
|
450
|
+
{ name: 'GEM', value: f.gem, desc: 'Canadian model' },
|
|
451
|
+
{ name: 'JMA', value: f.jma, desc: 'Japanese model (best for Asia)' },
|
|
452
|
+
{ name: 'ICON', value: f.icon, desc: 'German model' },
|
|
453
|
+
{ name: 'Météo-Fr', value: f.meteoFrance, desc: 'French model' },
|
|
454
|
+
{ name: 'Met Norway', value: f.metNorway, desc: 'Norwegian model' },
|
|
455
|
+
].filter(m => m.value !== undefined && m.value !== null);
|
|
456
|
+
const avg = f.average || 0;
|
|
457
|
+
const max = Math.max(...models.map(m => m.value || 0), 1);
|
|
458
|
+
const min = Math.min(...models.map(m => m.value || 0));
|
|
459
|
+
const spread = max - min;
|
|
460
|
+
const chart = models.map(m => {
|
|
461
|
+
const pct = max > 0 ? Math.round((m.value || 0) / max * 20) : 0;
|
|
462
|
+
const bar = '█'.repeat(pct) + '░'.repeat(20 - pct);
|
|
463
|
+
const diff = (m.value || 0) - avg;
|
|
464
|
+
const diffStr = diff >= 0 ? `+${diff.toFixed(0)}` : diff.toFixed(0);
|
|
465
|
+
return `${m.name.padEnd(10)} ${bar} ${(m.value || 0).toString().padStart(3)}cm (${diffStr})`;
|
|
466
|
+
}).join('\n');
|
|
467
|
+
const confidence = spread < 10 ? 'HIGH' : spread < 25 ? 'MEDIUM' : 'LOW';
|
|
468
|
+
const confidenceEmoji = confidence === 'HIGH' ? '🟢' : confidence === 'MEDIUM' ? '🟡' : '🔴';
|
|
469
|
+
return {
|
|
470
|
+
content: [
|
|
471
|
+
{
|
|
472
|
+
type: 'text',
|
|
473
|
+
text: `# 14-Day Forecast Comparison\n## ${resort.name}\n\n` +
|
|
474
|
+
`**SnowSure™ Average:** ${avg}cm\n` +
|
|
475
|
+
`**Model Spread:** ${spread.toFixed(0)}cm (${min}-${max}cm)\n` +
|
|
476
|
+
`**Confidence:** ${confidenceEmoji} ${confidence}\n\n` +
|
|
477
|
+
`\`\`\`\n${chart}\n\`\`\`\n\n` +
|
|
478
|
+
`### Model Notes\n` +
|
|
479
|
+
`- **ECMWF** is typically most accurate globally\n` +
|
|
480
|
+
`- **JMA** excels for Japan/Asia forecasts\n` +
|
|
481
|
+
`- **GEM** is strongest for North America\n` +
|
|
482
|
+
`- SnowSure uses weighted averages based on historical accuracy\n\n` +
|
|
483
|
+
`*${confidence === 'HIGH' ? 'Models agree closely - forecast is reliable' : confidence === 'MEDIUM' ? 'Some model disagreement - check closer to date' : 'High uncertainty - conditions may change significantly'}*`,
|
|
484
|
+
},
|
|
485
|
+
],
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
// === ADVANCED TOOLS ===
|
|
489
|
+
case 'get_weather_forecast': {
|
|
490
|
+
const data = await fetchAPI(`/resorts/${args?.slug}`);
|
|
491
|
+
const resort = data.data;
|
|
492
|
+
if (!resort?.forecast?.daily) {
|
|
493
|
+
return {
|
|
494
|
+
content: [{ type: 'text', text: 'Daily forecast not available for this resort.' }],
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
const days = Math.min(args?.days || 7, 14);
|
|
498
|
+
const forecast = resort.forecast.daily.slice(0, days);
|
|
499
|
+
const formatted = forecast.map((day, i) => {
|
|
500
|
+
const date = new Date();
|
|
501
|
+
date.setDate(date.getDate() + i);
|
|
502
|
+
const dayName = i === 0 ? 'Today' : i === 1 ? 'Tomorrow' : date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
|
503
|
+
return `### ${dayName}\n` +
|
|
504
|
+
`- High: ${day.tempMax || day.temp || 'N/A'}°C | Low: ${day.tempMin || 'N/A'}°C\n` +
|
|
505
|
+
`- Conditions: ${day.conditions || 'N/A'}\n` +
|
|
506
|
+
`- Snowfall: ${day.snowfall || 0}cm\n` +
|
|
507
|
+
`- Wind: ${day.windSpeed || 'N/A'} km/h`;
|
|
508
|
+
}).join('\n\n');
|
|
509
|
+
return {
|
|
510
|
+
content: [
|
|
511
|
+
{
|
|
512
|
+
type: 'text',
|
|
513
|
+
text: `# ${days}-Day Forecast: ${resort.name}\n\n${formatted}`,
|
|
514
|
+
},
|
|
515
|
+
],
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
case 'find_resorts_by_criteria': {
|
|
519
|
+
const data = await fetchAPI('/resorts?limit=200');
|
|
520
|
+
let results = data.data || [];
|
|
521
|
+
// Apply filters
|
|
522
|
+
if (args?.minScore != null) {
|
|
523
|
+
const min = Number(args.minScore);
|
|
524
|
+
results = results.filter((r) => (r.snowSure?.score || 0) >= min);
|
|
525
|
+
}
|
|
526
|
+
if (args?.minDepth != null) {
|
|
527
|
+
const min = Number(args.minDepth);
|
|
528
|
+
results = results.filter((r) => (r.conditions?.snowDepthCm || 0) >= min);
|
|
529
|
+
}
|
|
530
|
+
if (args?.minElevation != null) {
|
|
531
|
+
const min = Number(args.minElevation);
|
|
532
|
+
results = results.filter((r) => (r.elevation?.summit || 0) >= min);
|
|
533
|
+
}
|
|
534
|
+
if (args?.minRuns != null) {
|
|
535
|
+
const min = Number(args.minRuns);
|
|
536
|
+
results = results.filter((r) => (r.stats?.runs || 0) >= min);
|
|
537
|
+
}
|
|
538
|
+
if (args?.country) {
|
|
539
|
+
results = results.filter((r) => r.country?.toLowerCase() === args.country.toLowerCase());
|
|
540
|
+
}
|
|
541
|
+
if (args?.region) {
|
|
542
|
+
const regionMap = {
|
|
543
|
+
'europe': ['Switzerland', 'France', 'Austria', 'Italy', 'Germany', 'Norway'],
|
|
544
|
+
'north-america': ['United States', 'Canada'],
|
|
545
|
+
'asia': ['Japan'],
|
|
546
|
+
};
|
|
547
|
+
const countries = regionMap[args.region] || [];
|
|
548
|
+
if (countries.length > 0) {
|
|
549
|
+
results = results.filter((r) => countries.includes(r.country));
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
// Sort by score
|
|
553
|
+
results.sort((a, b) => (b.snowSure?.score || 0) - (a.snowSure?.score || 0));
|
|
554
|
+
const limit = args?.limit || 20;
|
|
555
|
+
const formatted = results.slice(0, limit).map((r, i) => `${i + 1}. **${r.name}** (${r.country})\n` +
|
|
556
|
+
` Score: ${r.snowSure?.score || 'N/A'} | Depth: ${r.conditions?.snowDepthCm || 'N/A'}cm | ` +
|
|
557
|
+
`Summit: ${r.elevation?.summit || 'N/A'}m | Runs: ${r.stats?.runs || 'N/A'}`).join('\n');
|
|
558
|
+
const filterDesc = [
|
|
559
|
+
args?.minScore && `Score ≥ ${args.minScore}`,
|
|
560
|
+
args?.minDepth && `Depth ≥ ${args.minDepth}cm`,
|
|
561
|
+
args?.minElevation && `Summit ≥ ${args.minElevation}m`,
|
|
562
|
+
args?.minRuns && `Runs ≥ ${args.minRuns}`,
|
|
563
|
+
args?.country && `Country: ${args.country}`,
|
|
564
|
+
args?.region && `Region: ${args.region}`,
|
|
565
|
+
].filter(Boolean).join(', ');
|
|
566
|
+
return {
|
|
567
|
+
content: [
|
|
568
|
+
{
|
|
569
|
+
type: 'text',
|
|
570
|
+
text: `# Resorts Matching Criteria\n\n**Filters:** ${filterDesc || 'None'}\n**Results:** ${results.length} resorts found\n\n${formatted || 'No resorts match your criteria.'}`,
|
|
571
|
+
},
|
|
572
|
+
],
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
case 'get_snow_history': {
|
|
576
|
+
const data = await fetchAPI(`/resorts/${args?.slug}`);
|
|
577
|
+
const resort = data.data;
|
|
578
|
+
if (!resort) {
|
|
579
|
+
return {
|
|
580
|
+
content: [{ type: 'text', text: `Resort "${args?.slug}" not found.` }],
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
const historic = resort.historic || {};
|
|
584
|
+
const snow = resort.snow || {};
|
|
585
|
+
const fiveYearAvg = historic.fiveYearAverage || 'N/A';
|
|
586
|
+
const thirtyYearAvg = historic.thirtyYearAverage || 'N/A';
|
|
587
|
+
const seasonTotal = snow.seasonTotal?.cm || 0;
|
|
588
|
+
let comparison = '';
|
|
589
|
+
if (typeof fiveYearAvg === 'number' && seasonTotal > 0) {
|
|
590
|
+
const pct5 = Math.round((seasonTotal / fiveYearAvg) * 100);
|
|
591
|
+
comparison += `- vs 5-Year Avg: ${pct5}% (${pct5 > 100 ? '+' : ''}${seasonTotal - fiveYearAvg}cm)\n`;
|
|
592
|
+
}
|
|
593
|
+
if (typeof thirtyYearAvg === 'number' && seasonTotal > 0) {
|
|
594
|
+
const pct30 = Math.round((seasonTotal / thirtyYearAvg) * 100);
|
|
595
|
+
comparison += `- vs 30-Year Avg: ${pct30}% (${pct30 > 100 ? '+' : ''}${seasonTotal - thirtyYearAvg}cm)\n`;
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
content: [
|
|
599
|
+
{
|
|
600
|
+
type: 'text',
|
|
601
|
+
text: `# Snow History: ${resort.name}\n\n` +
|
|
602
|
+
`## This Season\n` +
|
|
603
|
+
`- **Season Total:** ${seasonTotal}cm\n` +
|
|
604
|
+
`- **Last 7 Days:** ${snow.last7d?.cm || 0}cm\n` +
|
|
605
|
+
`- **Last 24h:** ${snow.last24h?.cm || 0}cm\n\n` +
|
|
606
|
+
`## Historical Averages\n` +
|
|
607
|
+
`- **5-Year Average:** ${fiveYearAvg}cm\n` +
|
|
608
|
+
`- **30-Year Average:** ${thirtyYearAvg}cm\n\n` +
|
|
609
|
+
`## Season Comparison\n${comparison || 'Not enough data for comparison.'}\n\n` +
|
|
610
|
+
`## Best Months to Visit\n` +
|
|
611
|
+
`${historic.bestMonths?.join(', ') || 'January, February, March (typical)'}\n\n` +
|
|
612
|
+
`${historic.trend ? `## Historical Trend\n${historic.trend}` : ''}`
|
|
613
|
+
},
|
|
614
|
+
],
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
case 'plan_ski_trip': {
|
|
618
|
+
const data = await fetchAPI('/snow-report?sort=snowsure&limit=50');
|
|
619
|
+
let resorts = data.data?.resorts || data.data || [];
|
|
620
|
+
// Filter by region
|
|
621
|
+
if (args?.region && args.region !== 'any') {
|
|
622
|
+
const regionMap = {
|
|
623
|
+
'europe': ['Switzerland', 'France', 'Austria', 'Italy', 'Germany', 'Norway'],
|
|
624
|
+
'north-america': ['United States', 'Canada'],
|
|
625
|
+
'asia': ['Japan'],
|
|
626
|
+
};
|
|
627
|
+
const countries = regionMap[args.region] || [];
|
|
628
|
+
if (countries.length > 0) {
|
|
629
|
+
resorts = resorts.filter((r) => countries.includes(r.country));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
// Get top recommendations
|
|
633
|
+
const recommendations = resorts.slice(0, 5);
|
|
634
|
+
const formatted = recommendations.map((r, i) => `### ${i + 1}. ${r.name} (${r.country})\n` +
|
|
635
|
+
`- **SnowSure Score:** ${r.snowSure?.score || 'N/A'}/100 - ${r.snowSure?.rating || 'N/A'}\n` +
|
|
636
|
+
`- **Current Depth:** ${r.conditions?.snowDepthCm || 'N/A'}cm\n` +
|
|
637
|
+
`- **14-Day Forecast:** ${Math.round(r.conditions?.forecast14dCm || 0)}cm expected\n` +
|
|
638
|
+
`- **Why:** ${r.snowSure?.tagline || 'Great conditions'}`).join('\n\n');
|
|
639
|
+
return {
|
|
640
|
+
content: [
|
|
641
|
+
{
|
|
642
|
+
type: 'text',
|
|
643
|
+
text: `# 🎿 Ski Trip Recommendations\n\n` +
|
|
644
|
+
`**Region:** ${args?.region || 'Worldwide'}\n` +
|
|
645
|
+
`**Dates:** ${args?.dates || 'Flexible'}\n` +
|
|
646
|
+
`**Level:** ${args?.level || 'All levels'}\n\n` +
|
|
647
|
+
`## Top Picks\n\n${formatted}\n\n` +
|
|
648
|
+
`## Tips\n` +
|
|
649
|
+
`- Book accommodation 2-4 weeks ahead for best rates\n` +
|
|
650
|
+
`- Check forecasts 7 days before departure\n` +
|
|
651
|
+
`- Consider mid-week dates for shorter lift lines\n` +
|
|
652
|
+
`- SnowSure scores above 50 indicate excellent conditions`,
|
|
653
|
+
},
|
|
654
|
+
],
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
case 'get_webcam_status': {
|
|
658
|
+
const data = await fetchAPI(`/resorts/${args?.slug}`);
|
|
659
|
+
const resort = data.data;
|
|
660
|
+
if (!resort) {
|
|
661
|
+
return {
|
|
662
|
+
content: [{ type: 'text', text: `Resort "${args?.slug}" not found.` }],
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
const webcams = resort.webcams || [];
|
|
666
|
+
const webcamPage = resort.links?.webcamPage;
|
|
667
|
+
if (!webcams.length && !webcamPage) {
|
|
668
|
+
return {
|
|
669
|
+
content: [{ type: 'text', text: `No webcam data available for ${resort.name}.` }],
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
const formatted = webcams.map((cam) => `- **${cam.name || 'Webcam'}**\n` +
|
|
673
|
+
` URL: ${cam.url || cam.embedUrl || 'N/A'}\n` +
|
|
674
|
+
` ${cam.thumbnailUrl ? `Thumbnail: ${cam.thumbnailUrl}` : ''}`).join('\n');
|
|
675
|
+
return {
|
|
676
|
+
content: [
|
|
677
|
+
{
|
|
678
|
+
type: 'text',
|
|
679
|
+
text: `# Webcams: ${resort.name}\n\n` +
|
|
680
|
+
`${webcams.length ? formatted : 'Individual webcams not indexed.'}\n\n` +
|
|
681
|
+
`${webcamPage ? `**Resort Webcam Page:** ${webcamPage}` : ''}\n\n` +
|
|
682
|
+
`*View live at: https://snowsure.ai/resorts/${resort.slug}#webcams*`,
|
|
683
|
+
},
|
|
684
|
+
],
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
case 'get_regional_summary': {
|
|
688
|
+
const data = await fetchAPI('/resorts?limit=200');
|
|
689
|
+
let resorts = data.data || [];
|
|
690
|
+
// Filter by region or country
|
|
691
|
+
if (args?.region) {
|
|
692
|
+
const regionMap = {
|
|
693
|
+
'europe': ['Switzerland', 'France', 'Austria', 'Italy', 'Germany', 'Norway', 'Sweden'],
|
|
694
|
+
'north-america': ['United States', 'Canada'],
|
|
695
|
+
'asia': ['Japan', 'South Korea'],
|
|
696
|
+
'alps': ['Switzerland', 'France', 'Austria', 'Italy'],
|
|
697
|
+
'rockies': ['United States', 'Canada'],
|
|
698
|
+
'japan': ['Japan'],
|
|
699
|
+
};
|
|
700
|
+
const countries = regionMap[args.region] || [];
|
|
701
|
+
if (countries.length > 0) {
|
|
702
|
+
resorts = resorts.filter((r) => countries.includes(r.country));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (args?.country) {
|
|
706
|
+
resorts = resorts.filter((r) => r.country?.toLowerCase() === args.country.toLowerCase());
|
|
707
|
+
}
|
|
708
|
+
// Calculate stats
|
|
709
|
+
const totalResorts = resorts.length;
|
|
710
|
+
const withSnow = resorts.filter((r) => (r.conditions?.snowDepthCm || 0) > 0).length;
|
|
711
|
+
const avgDepth = resorts.reduce((sum, r) => sum + (r.conditions?.snowDepthCm || 0), 0) / totalResorts;
|
|
712
|
+
const avgScore = resorts.reduce((sum, r) => sum + (r.snowSure?.score || 0), 0) / totalResorts;
|
|
713
|
+
const totalFresh = resorts.reduce((sum, r) => sum + (r.conditions?.snowfall24hCm || 0), 0);
|
|
714
|
+
// Top resorts
|
|
715
|
+
const topByScore = [...resorts].sort((a, b) => (b.snowSure?.score || 0) - (a.snowSure?.score || 0)).slice(0, 5);
|
|
716
|
+
const topBySnow = [...resorts].sort((a, b) => (b.conditions?.snowfall24hCm || 0) - (a.conditions?.snowfall24hCm || 0)).slice(0, 5);
|
|
717
|
+
return {
|
|
718
|
+
content: [
|
|
719
|
+
{
|
|
720
|
+
type: 'text',
|
|
721
|
+
text: `# Regional Summary: ${args?.region || args?.country || 'Global'}\n\n` +
|
|
722
|
+
`## Statistics\n` +
|
|
723
|
+
`- **Total Resorts:** ${totalResorts}\n` +
|
|
724
|
+
`- **Resorts with Snow:** ${withSnow}\n` +
|
|
725
|
+
`- **Average Depth:** ${avgDepth.toFixed(0)}cm\n` +
|
|
726
|
+
`- **Average SnowSure Score:** ${avgScore.toFixed(0)}/100\n` +
|
|
727
|
+
`- **Fresh Snow (24h total):** ${totalFresh.toFixed(0)}cm across region\n\n` +
|
|
728
|
+
`## Top by SnowSure Score\n` +
|
|
729
|
+
topByScore.map((r, i) => `${i + 1}. ${r.name} - ${r.snowSure?.score || 'N/A'}/100`).join('\n') +
|
|
730
|
+
`\n\n## Most Fresh Snow (24h)\n` +
|
|
731
|
+
topBySnow.filter((r) => r.conditions?.snowfall24hCm > 0).map((r, i) => `${i + 1}. ${r.name} - +${r.conditions?.snowfall24hCm}cm`).join('\n') || 'No fresh snow reported',
|
|
732
|
+
},
|
|
733
|
+
],
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
default:
|
|
737
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
catch (error) {
|
|
741
|
+
return {
|
|
742
|
+
content: [
|
|
743
|
+
{
|
|
744
|
+
type: 'text',
|
|
745
|
+
text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
746
|
+
},
|
|
747
|
+
],
|
|
748
|
+
isError: true,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
// List available resources
|
|
753
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
754
|
+
resources: [
|
|
755
|
+
{
|
|
756
|
+
uri: 'snowsure://snow-report',
|
|
757
|
+
name: 'Global Snow Report',
|
|
758
|
+
description: 'Current snow conditions and rankings for top 50 resorts worldwide',
|
|
759
|
+
mimeType: 'application/json',
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
uri: 'snowsure://resorts',
|
|
763
|
+
name: 'All Resorts',
|
|
764
|
+
description: 'Complete list of 500+ ski resorts with current conditions',
|
|
765
|
+
mimeType: 'application/json',
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
uri: 'snowsure://regions',
|
|
769
|
+
name: 'Available Regions',
|
|
770
|
+
description: 'List of regions and countries with resort counts',
|
|
771
|
+
mimeType: 'application/json',
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
uri: 'snowsure://api-docs',
|
|
775
|
+
name: 'API Documentation',
|
|
776
|
+
description: 'OpenAPI specification for the SnowSure API',
|
|
777
|
+
mimeType: 'application/json',
|
|
778
|
+
},
|
|
779
|
+
],
|
|
780
|
+
}));
|
|
781
|
+
// Read resources
|
|
782
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
783
|
+
const { uri } = request.params;
|
|
784
|
+
switch (uri) {
|
|
785
|
+
case 'snowsure://snow-report': {
|
|
786
|
+
const data = await fetchAPI('/snow-report?limit=50');
|
|
787
|
+
return {
|
|
788
|
+
contents: [
|
|
789
|
+
{
|
|
790
|
+
uri,
|
|
791
|
+
mimeType: 'application/json',
|
|
792
|
+
text: JSON.stringify(data.data, null, 2),
|
|
793
|
+
},
|
|
794
|
+
],
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
case 'snowsure://resorts': {
|
|
798
|
+
const data = await fetchAPI('/resorts?limit=500');
|
|
799
|
+
return {
|
|
800
|
+
contents: [
|
|
801
|
+
{
|
|
802
|
+
uri,
|
|
803
|
+
mimeType: 'application/json',
|
|
804
|
+
text: JSON.stringify(data.data, null, 2),
|
|
805
|
+
},
|
|
806
|
+
],
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
case 'snowsure://regions': {
|
|
810
|
+
const data = await fetchAPI('/resorts?limit=500');
|
|
811
|
+
const resorts = data.data || [];
|
|
812
|
+
// Group by country and region
|
|
813
|
+
const byCountry = {};
|
|
814
|
+
const byRegion = {
|
|
815
|
+
'europe': [],
|
|
816
|
+
'north-america': [],
|
|
817
|
+
'asia': [],
|
|
818
|
+
'oceania': [],
|
|
819
|
+
};
|
|
820
|
+
resorts.forEach((r) => {
|
|
821
|
+
byCountry[r.country] = (byCountry[r.country] || 0) + 1;
|
|
822
|
+
// Map to regions
|
|
823
|
+
if (['Switzerland', 'France', 'Austria', 'Italy', 'Germany', 'Norway', 'Sweden'].includes(r.country)) {
|
|
824
|
+
if (!byRegion['europe'].includes(r.country))
|
|
825
|
+
byRegion['europe'].push(r.country);
|
|
826
|
+
}
|
|
827
|
+
else if (['United States', 'Canada'].includes(r.country)) {
|
|
828
|
+
if (!byRegion['north-america'].includes(r.country))
|
|
829
|
+
byRegion['north-america'].push(r.country);
|
|
830
|
+
}
|
|
831
|
+
else if (['Japan'].includes(r.country)) {
|
|
832
|
+
if (!byRegion['asia'].includes(r.country))
|
|
833
|
+
byRegion['asia'].push(r.country);
|
|
834
|
+
}
|
|
835
|
+
else if (['New Zealand', 'Australia'].includes(r.country)) {
|
|
836
|
+
if (!byRegion['oceania'].includes(r.country))
|
|
837
|
+
byRegion['oceania'].push(r.country);
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
return {
|
|
841
|
+
contents: [
|
|
842
|
+
{
|
|
843
|
+
uri,
|
|
844
|
+
mimeType: 'application/json',
|
|
845
|
+
text: JSON.stringify({
|
|
846
|
+
totalResorts: resorts.length,
|
|
847
|
+
byCountry,
|
|
848
|
+
byRegion,
|
|
849
|
+
}, null, 2),
|
|
850
|
+
},
|
|
851
|
+
],
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
case 'snowsure://api-docs': {
|
|
855
|
+
return {
|
|
856
|
+
contents: [
|
|
857
|
+
{
|
|
858
|
+
uri,
|
|
859
|
+
mimeType: 'application/json',
|
|
860
|
+
text: JSON.stringify({
|
|
861
|
+
openapi: '3.1.0',
|
|
862
|
+
info: {
|
|
863
|
+
title: 'SnowSure API',
|
|
864
|
+
version: '3.0.0',
|
|
865
|
+
description: 'Real-time snow conditions for 220+ ski resorts worldwide. Data sourced from SnowSure\'s Sanity database with multi-model weather forecasts.',
|
|
866
|
+
},
|
|
867
|
+
servers: [{ url: 'https://www.snowsure.ai/api/v1' }],
|
|
868
|
+
endpoints: {
|
|
869
|
+
'/resorts': 'List all resorts with current conditions',
|
|
870
|
+
'/resorts/{slug}': 'Get detailed resort information including forecasts',
|
|
871
|
+
'/snow-report': 'Get global snow rankings sorted by various criteria',
|
|
872
|
+
},
|
|
873
|
+
website: 'https://www.snowsure.ai',
|
|
874
|
+
}, null, 2),
|
|
875
|
+
},
|
|
876
|
+
],
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
default:
|
|
880
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
// Start the server
|
|
884
|
+
async function main() {
|
|
885
|
+
const transport = new StdioServerTransport();
|
|
886
|
+
await server.connect(transport);
|
|
887
|
+
console.error('SnowSure MCP server v3.0 running');
|
|
888
|
+
console.error('API: https://www.snowsure.ai/api/v1');
|
|
889
|
+
console.error('Tools: 11 | Resources: 4');
|
|
890
|
+
}
|
|
891
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "snowsure-mcp-server",
|
|
3
|
+
"version": "3.0.1",
|
|
4
|
+
"description": "MCP server for ski resort snow: real-time powder rankings, 14-day forecasts, 500+ resorts — SnowSure.ai (Cursor, Claude, AI agents)",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"snowsure-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "tsx src/index.ts",
|
|
14
|
+
"watch": "tsc --watch"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"model-context-protocol",
|
|
19
|
+
"cursor",
|
|
20
|
+
"claude",
|
|
21
|
+
"openai",
|
|
22
|
+
"ski",
|
|
23
|
+
"ski-resort",
|
|
24
|
+
"snow",
|
|
25
|
+
"powder",
|
|
26
|
+
"weather",
|
|
27
|
+
"forecast",
|
|
28
|
+
"snowsure"
|
|
29
|
+
],
|
|
30
|
+
"author": "SnowSure",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"homepage": "https://www.snowsure.ai/developers",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/mikeslone/snowsure-web.git",
|
|
36
|
+
"directory": "mcp-server"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/mikeslone/snowsure-web/issues"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^20.11.0",
|
|
46
|
+
"tsx": "^4.7.0",
|
|
47
|
+
"typescript": "^5.3.3"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18.0.0"
|
|
51
|
+
},
|
|
52
|
+
"files": [
|
|
53
|
+
"dist",
|
|
54
|
+
"README.md"
|
|
55
|
+
]
|
|
56
|
+
}
|