@rundida/mcp-server 1.0.0

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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -0
  3. package/index.js +341 -0
  4. package/package.json +49 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 RunDida
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # @rundida/mcp-server
2
+
3
+ MCP server for [**RunDida**](https://rundida.com) — the world's most comprehensive running tools platform.
4
+
5
+ Give your AI assistant access to 86 running calculators, 29 marathon events, pace/time/distance calculations, race time predictions, and heart rate training zones.
6
+
7
+ [![Website](https://img.shields.io/badge/RunDida.com-86%20Running%20Tools-blue)](https://rundida.com)
8
+ [![npm](https://img.shields.io/npm/v/@rundida/mcp-server)](https://www.npmjs.com/package/@rundida/mcp-server)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
10
+
11
+ ## Quick Start
12
+
13
+ ### Claude Desktop
14
+
15
+ Add to your `claude_desktop_config.json`:
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "rundida": {
21
+ "command": "npx",
22
+ "args": ["-y", "@rundida/mcp-server"]
23
+ }
24
+ }
25
+ }
26
+ ```
27
+
28
+ ### Claude Code
29
+
30
+ ```bash
31
+ claude mcp add rundida -- npx -y @rundida/mcp-server
32
+ ```
33
+
34
+ ### Cursor / Windsurf
35
+
36
+ Add to your MCP configuration:
37
+
38
+ ```json
39
+ {
40
+ "rundida": {
41
+ "command": "npx",
42
+ "args": ["-y", "@rundida/mcp-server"]
43
+ }
44
+ }
45
+ ```
46
+
47
+ ## Available Tools
48
+
49
+ | Tool | Type | Description |
50
+ |------|------|-------------|
51
+ | `list_tools` | Data | Browse all [86 running calculators](https://rundida.com) with descriptions |
52
+ | `get_tool` | Data | Get details, FAQs, and sources for a specific tool |
53
+ | `list_marathons` | Data | List [29 marathon events](https://rundida.com/marathon/) with dates and locations |
54
+ | `get_marathon` | Data | Get marathon details including weather and course profile |
55
+ | `calculate_pace` | Compute | Calculate pace, time, or distance (provide any 2 of 3) |
56
+ | `predict_race` | Compute | Predict race times using Riegel formula + VO2max estimation |
57
+ | `heart_rate_zones` | Compute | Calculate 5 HR training zones (Karvonen method) |
58
+ | `marathon_countdown` | Compute | Get countdown to a specific marathon event |
59
+
60
+ **Data tools** fetch from the [RunDida API](https://rundida.com/api/) with 30-minute caching. **Compute tools** run locally with zero latency — no API calls needed.
61
+
62
+ ## Example Usage
63
+
64
+ Ask your AI assistant:
65
+
66
+ - "What's my marathon pace if I want to finish in 3:30?"
67
+ - "Predict my marathon time based on my 10K of 45 minutes"
68
+ - "What are my heart rate zones? I'm 32 with a resting HR of 52"
69
+ - "How many days until the Tokyo Marathon?"
70
+ - "Show me all running calculators related to nutrition"
71
+
72
+ ## About RunDida
73
+
74
+ [**RunDida**](https://rundida.com) (跑滴答) is a free running tools platform for runners of all levels:
75
+
76
+ - **[86 Interactive Calculators](https://rundida.com)** — Pace, heart rate zones, VO2max, race prediction, nutrition, gear sizing, weather impact, and more
77
+ - **[29 Marathon Countdowns](https://rundida.com/marathon/)** — Live timers with race-day weather forecasts, course profiles, and training tools
78
+ - **[Free JSON API](https://rundida.com/api/)** — No authentication required, CORS enabled, [OpenAPI 3.0 documented](https://rundida.com/api/openapi.json)
79
+ - **Multi-language** — English, Chinese (中文)
80
+ - **Embeddable Widgets** — One-line iframe embed for any calculator
81
+
82
+ All tools are free, no account required. Try them at [rundida.com](https://rundida.com).
83
+
84
+ ## How It Works
85
+
86
+ The computation tools use established running science formulas:
87
+
88
+ | Formula | Used In | Description |
89
+ |---------|---------|-------------|
90
+ | **Riegel formula** | `predict_race` | Race time prediction across distances |
91
+ | **Jack Daniels method** | `predict_race` | VO2max estimation from race performance |
92
+ | **Karvonen method** | `heart_rate_zones` | Heart rate training zones from age and resting HR |
93
+
94
+ ## Requirements
95
+
96
+ - Node.js >= 18
97
+ - Internet connection (data tools fetch from [rundida.com](https://rundida.com))
98
+
99
+ ## Links
100
+
101
+ | Resource | URL |
102
+ |----------|-----|
103
+ | **RunDida Website** | [rundida.com](https://rundida.com) |
104
+ | **API Documentation** | [rundida.com/api](https://rundida.com/api/) |
105
+ | **OpenAPI Spec** | [rundida.com/api/openapi.json](https://rundida.com/api/openapi.json) |
106
+ | **NPM Package** | [@rundida/mcp-server](https://www.npmjs.com/package/@rundida/mcp-server) |
107
+
108
+ ## License
109
+
110
+ MIT
package/index.js ADDED
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import { z } from 'zod';
6
+
7
+ const BASE_URL = 'https://rundida.com';
8
+ const CACHE_TTL = 30 * 60 * 1000; // 30 minutes
9
+
10
+ // Simple in-memory cache
11
+ const cache = new Map();
12
+
13
+ async function fetchJSON(url) {
14
+ const cached = cache.get(url);
15
+ if (cached && Date.now() - cached.ts < CACHE_TTL) return cached.data;
16
+
17
+ const res = await fetch(url);
18
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
19
+ const data = await res.json();
20
+ cache.set(url, { data, ts: Date.now() });
21
+ return data;
22
+ }
23
+
24
+ // ---- Pace calculation helpers ----
25
+
26
+ function paceToSeconds(pace) {
27
+ const [m, s] = pace.split(':').map(Number);
28
+ return m * 60 + (s || 0);
29
+ }
30
+
31
+ function secondsToPace(secs) {
32
+ const m = Math.floor(secs / 60);
33
+ const s = Math.round(secs % 60);
34
+ return `${m}:${s.toString().padStart(2, '0')}`;
35
+ }
36
+
37
+ function timeToSeconds(time) {
38
+ const parts = time.split(':').map(Number);
39
+ if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
40
+ if (parts.length === 2) return parts[0] * 60 + parts[1];
41
+ return parts[0];
42
+ }
43
+
44
+ function secondsToTime(secs) {
45
+ const h = Math.floor(secs / 3600);
46
+ const m = Math.floor((secs % 3600) / 60);
47
+ const s = Math.round(secs % 60);
48
+ if (h > 0) return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
49
+ return `${m}:${s.toString().padStart(2, '0')}`;
50
+ }
51
+
52
+ const DISTANCES = {
53
+ '5k': 5,
54
+ '10k': 10,
55
+ 'half': 21.0975,
56
+ 'half-marathon': 21.0975,
57
+ 'marathon': 42.195,
58
+ '50k': 50,
59
+ '100k': 100,
60
+ };
61
+
62
+ function resolveDistance(input) {
63
+ const lower = String(input).toLowerCase().trim();
64
+ if (DISTANCES[lower]) return DISTANCES[lower];
65
+ const num = parseFloat(lower);
66
+ if (!isNaN(num) && num > 0) return num;
67
+ throw new Error(`Unknown distance: ${input}. Use 5k, 10k, half, marathon, or a number in km.`);
68
+ }
69
+
70
+ // ---- Riegel race prediction ----
71
+
72
+ function predictTime(knownDist, knownTimeSecs, targetDist, exponent = 1.06) {
73
+ return knownTimeSecs * Math.pow(targetDist / knownDist, exponent);
74
+ }
75
+
76
+ // ---- VO2max estimation (Jack Daniels) ----
77
+
78
+ function estimateVO2max(distKm, timeSecs) {
79
+ const timeMin = timeSecs / 60;
80
+ const velocity = distKm * 1000 / timeMin; // meters per minute
81
+ const pctVO2max = 0.8 + 0.1894393 * Math.exp(-0.012778 * timeMin)
82
+ + 0.2989558 * Math.exp(-0.1932605 * timeMin);
83
+ const vo2 = -4.60 + 0.182258 * velocity + 0.000104 * velocity * velocity;
84
+ return vo2 / pctVO2max;
85
+ }
86
+
87
+ // ---- Heart Rate Zones (Karvonen) ----
88
+
89
+ function heartRateZones(age, restingHR, maxHR) {
90
+ const mhr = maxHR || Math.round(207 - 0.7 * age);
91
+ const hrr = mhr - restingHR;
92
+ const zones = [
93
+ { zone: 1, name: 'Recovery', low: 0.50, high: 0.60 },
94
+ { zone: 2, name: 'Aerobic', low: 0.60, high: 0.70 },
95
+ { zone: 3, name: 'Tempo', low: 0.70, high: 0.80 },
96
+ { zone: 4, name: 'Threshold', low: 0.80, high: 0.90 },
97
+ { zone: 5, name: 'VO2max', low: 0.90, high: 1.00 },
98
+ ];
99
+ return {
100
+ maxHR: mhr,
101
+ restingHR,
102
+ zones: zones.map(z => ({
103
+ ...z,
104
+ bpmLow: Math.round(restingHR + hrr * z.low),
105
+ bpmHigh: Math.round(restingHR + hrr * z.high),
106
+ })),
107
+ };
108
+ }
109
+
110
+ // ---- Create Server ----
111
+
112
+ const server = new McpServer({
113
+ name: 'RunDida',
114
+ version: '1.0.0',
115
+ });
116
+
117
+ // Tool: list_tools
118
+ server.tool(
119
+ 'list_tools',
120
+ 'List all available running calculators and tools on RunDida',
121
+ {},
122
+ async () => {
123
+ const data = await fetchJSON(`${BASE_URL}/api/tools.json`);
124
+ const list = data.tools.map(t => `- ${t.title} (${t.slug}): ${t.description}`).join('\n');
125
+ return {
126
+ content: [{
127
+ type: 'text',
128
+ text: `RunDida has ${data.meta.total} running tools:\n\n${list}\n\nUse get_tool with a slug to see full details.`,
129
+ }],
130
+ };
131
+ }
132
+ );
133
+
134
+ // Tool: get_tool
135
+ server.tool(
136
+ 'get_tool',
137
+ 'Get detailed information about a specific running tool including FAQs and related tools',
138
+ { slug: z.string().describe('Tool slug, e.g. "pace-calculator", "heart-rate-zones"') },
139
+ async ({ slug }) => {
140
+ const data = await fetchJSON(`${BASE_URL}/api/tools/${slug}.json`);
141
+ const t = data.tool;
142
+ let text = `## ${t.title}\n\n${t.description}\n\nURL: ${t.url}\nCategory: ${t.category}\n`;
143
+ if (t.relatedTools.length) text += `\nRelated tools: ${t.relatedTools.join(', ')}`;
144
+ if (t.faqs.length) {
145
+ text += '\n\n### FAQs\n';
146
+ t.faqs.forEach(f => { text += `\n**Q: ${f.question}**\nA: ${f.answer}\n`; });
147
+ }
148
+ return { content: [{ type: 'text', text }] };
149
+ }
150
+ );
151
+
152
+ // Tool: list_marathons
153
+ server.tool(
154
+ 'list_marathons',
155
+ 'List all marathon events tracked by RunDida with dates and locations',
156
+ {},
157
+ async () => {
158
+ const data = await fetchJSON(`${BASE_URL}/api/marathons.json`);
159
+ const list = data.active
160
+ .sort((a, b) => a.date.localeCompare(b.date))
161
+ .map(m => `- ${m.name.en} — ${m.date} — ${m.city}`)
162
+ .join('\n');
163
+ return {
164
+ content: [{
165
+ type: 'text',
166
+ text: `RunDida tracks ${data.meta.totalActive} upcoming marathons:\n\n${list}\n\nUse get_marathon with an ID for details.`,
167
+ }],
168
+ };
169
+ }
170
+ );
171
+
172
+ // Tool: get_marathon
173
+ server.tool(
174
+ 'get_marathon',
175
+ 'Get detailed information about a specific marathon including countdown and Schema.org data',
176
+ { id: z.string().describe('Marathon ID, e.g. "tokyo2026", "boston2026", "berlin2026"') },
177
+ async ({ id }) => {
178
+ const data = await fetchJSON(`${BASE_URL}/api/marathons/${id}.json`);
179
+ const m = data.marathon;
180
+ const raceDate = new Date(m.date);
181
+ const now = new Date();
182
+ const daysUntil = Math.ceil((raceDate - now) / (1000 * 60 * 60 * 24));
183
+ let text = `## ${m.name.en}\n\nDate: ${m.date}\nCity: ${m.city}\n`;
184
+ if (m.country) text += `Country/Region: ${m.country}\n`;
185
+ text += `Timezone: ${m.timezone}\n`;
186
+ text += `Days until race: ${daysUntil > 0 ? daysUntil : 'Race has passed'}\n`;
187
+ if (m.weather) {
188
+ text += `\n### Race Day Weather\nTemperature: ${m.weather.avgTempC}°C / ${m.weather.avgTempF}°F\n`;
189
+ text += `Humidity: ${m.weather.humidity}% | Wind: ${m.weather.windKmh} km/h | Rain: ${m.weather.precipPct}%\n`;
190
+ text += `Conditions: ${m.weather.conditions}\n`;
191
+ }
192
+ if (m.course) {
193
+ text += `\n### Course Profile\nType: ${m.course.type} | Elevation: ${m.course.elevationGain}m | Terrain: ${m.course.terrain}\n`;
194
+ text += `${m.course.profile}\n`;
195
+ }
196
+ text += `\nPage: ${m.links.page}\nCountdown: ${m.links.countdown}\n`;
197
+ return { content: [{ type: 'text', text }] };
198
+ }
199
+ );
200
+
201
+ // Tool: calculate_pace
202
+ server.tool(
203
+ 'calculate_pace',
204
+ 'Calculate running pace, finish time, or distance. Provide any two of: distance, time, pace.',
205
+ {
206
+ distance: z.string().optional().describe('Distance: "5k", "10k", "half", "marathon", or km value like "15"'),
207
+ time: z.string().optional().describe('Finish time in H:MM:SS or MM:SS format, e.g. "3:30:00" or "25:00"'),
208
+ pace: z.string().optional().describe('Pace per km in M:SS format, e.g. "5:00"'),
209
+ },
210
+ async ({ distance, time, pace }) => {
211
+ const given = [distance, time, pace].filter(Boolean).length;
212
+ if (given < 2) throw new Error('Provide at least 2 of: distance, time, pace');
213
+
214
+ let result = {};
215
+
216
+ if (distance && time) {
217
+ const distKm = resolveDistance(distance);
218
+ const timeSecs = timeToSeconds(time);
219
+ const paceSecs = timeSecs / distKm;
220
+ result = {
221
+ distance: `${distKm} km`,
222
+ time: secondsToTime(timeSecs),
223
+ pace: `${secondsToPace(paceSecs)}/km`,
224
+ speed: `${(distKm / (timeSecs / 3600)).toFixed(2)} km/h`,
225
+ };
226
+ } else if (distance && pace) {
227
+ const distKm = resolveDistance(distance);
228
+ const paceSecs = paceToSeconds(pace);
229
+ const timeSecs = distKm * paceSecs;
230
+ result = {
231
+ distance: `${distKm} km`,
232
+ time: secondsToTime(timeSecs),
233
+ pace: `${secondsToPace(paceSecs)}/km`,
234
+ speed: `${(distKm / (timeSecs / 3600)).toFixed(2)} km/h`,
235
+ };
236
+ } else if (time && pace) {
237
+ const timeSecs = timeToSeconds(time);
238
+ const paceSecs = paceToSeconds(pace);
239
+ const distKm = timeSecs / paceSecs;
240
+ result = {
241
+ distance: `${distKm.toFixed(2)} km`,
242
+ time: secondsToTime(timeSecs),
243
+ pace: `${secondsToPace(paceSecs)}/km`,
244
+ speed: `${(distKm / (timeSecs / 3600)).toFixed(2)} km/h`,
245
+ };
246
+ }
247
+
248
+ return {
249
+ content: [{
250
+ type: 'text',
251
+ text: Object.entries(result).map(([k, v]) => `${k}: ${v}`).join('\n'),
252
+ }],
253
+ };
254
+ }
255
+ );
256
+
257
+ // Tool: predict_race
258
+ server.tool(
259
+ 'predict_race',
260
+ 'Predict race finish times using the Riegel formula based on a known race result',
261
+ {
262
+ known_distance: z.string().describe('Known race distance: "5k", "10k", "half", "marathon", or km value'),
263
+ known_time: z.string().describe('Known race time in H:MM:SS or MM:SS format'),
264
+ target_distance: z.string().optional().describe('Target distance to predict (defaults to showing all standard distances)'),
265
+ },
266
+ async ({ known_distance, known_time, target_distance }) => {
267
+ const distKm = resolveDistance(known_distance);
268
+ const timeSecs = timeToSeconds(known_time);
269
+ const vo2max = estimateVO2max(distKm, timeSecs);
270
+
271
+ const targets = target_distance
272
+ ? [{ name: target_distance, km: resolveDistance(target_distance) }]
273
+ : [
274
+ { name: '5K', km: 5 },
275
+ { name: '10K', km: 10 },
276
+ { name: 'Half Marathon', km: 21.0975 },
277
+ { name: 'Marathon', km: 42.195 },
278
+ ];
279
+
280
+ let text = `Based on ${known_distance} in ${known_time}:\n\nEstimated VO2max: ${vo2max.toFixed(1)} ml/kg/min\n\nPredictions:\n`;
281
+ targets.forEach(t => {
282
+ const predicted = predictTime(distKm, timeSecs, t.km);
283
+ const pace = predicted / t.km;
284
+ text += `- ${t.name}: ${secondsToTime(predicted)} (pace: ${secondsToPace(pace)}/km)\n`;
285
+ });
286
+
287
+ text += `\nNote: Predictions assume equivalent training for the target distance. Use RunDida's Race Time Predictor for more details: ${BASE_URL}/tools/race-time-predictor/`;
288
+ return { content: [{ type: 'text', text }] };
289
+ }
290
+ );
291
+
292
+ // Tool: heart_rate_zones
293
+ server.tool(
294
+ 'heart_rate_zones',
295
+ 'Calculate heart rate training zones using the Karvonen method',
296
+ {
297
+ age: z.number().min(10).max(99).describe('Your age in years'),
298
+ resting_hr: z.number().min(30).max(120).optional().describe('Resting heart rate in bpm (default: 60)'),
299
+ max_hr: z.number().min(100).max(220).optional().describe('Known max heart rate in bpm (auto-calculated if omitted)'),
300
+ },
301
+ async ({ age, resting_hr, max_hr }) => {
302
+ const result = heartRateZones(age, resting_hr || 60, max_hr);
303
+ let text = `Heart Rate Training Zones (Karvonen Method)\n\nAge: ${age} | Resting HR: ${result.restingHR} bpm | Max HR: ${result.maxHR} bpm\n\n`;
304
+ result.zones.forEach(z => {
305
+ text += `Zone ${z.zone} (${z.name}): ${z.bpmLow}-${z.bpmHigh} bpm\n`;
306
+ });
307
+ text += `\nFor more options: ${BASE_URL}/tools/heart-rate-zones/`;
308
+ return { content: [{ type: 'text', text }] };
309
+ }
310
+ );
311
+
312
+ // Tool: marathon_countdown
313
+ server.tool(
314
+ 'marathon_countdown',
315
+ 'Get a countdown to a specific marathon event',
316
+ { id: z.string().describe('Marathon ID, e.g. "tokyo2026", "boston2026"') },
317
+ async ({ id }) => {
318
+ const data = await fetchJSON(`${BASE_URL}/api/marathons/${id}.json`);
319
+ const m = data.marathon;
320
+ const raceDate = new Date(m.date + 'T00:00:00');
321
+ const now = new Date();
322
+ const diff = raceDate - now;
323
+
324
+ if (diff <= 0) {
325
+ return { content: [{ type: 'text', text: `${m.name.en} has already taken place on ${m.date}.` }] };
326
+ }
327
+
328
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
329
+ const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
330
+
331
+ let text = `${m.name.en} Countdown\n\n`;
332
+ text += `${days} days, ${hours} hours until race day\n`;
333
+ text += `Date: ${m.date}\nCity: ${m.city}\n`;
334
+ text += `\nLive countdown: ${m.links.countdown}`;
335
+ return { content: [{ type: 'text', text }] };
336
+ }
337
+ );
338
+
339
+ // Start server
340
+ const transport = new StdioServerTransport();
341
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@rundida/mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for RunDida running tools — marathon data, pace calculations, heart rate zones, and race predictions for AI agents",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "rundida-mcp": "./index.js"
9
+ },
10
+ "files": [
11
+ "index.js",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "scripts": {
19
+ "start": "node index.js"
20
+ },
21
+ "keywords": [
22
+ "mcp",
23
+ "model-context-protocol",
24
+ "running",
25
+ "marathon",
26
+ "pace-calculator",
27
+ "heart-rate-zones",
28
+ "race-prediction",
29
+ "vo2max",
30
+ "ai-tools",
31
+ "claude",
32
+ "llm",
33
+ "fitness"
34
+ ],
35
+ "author": "RunDida (https://rundida.com)",
36
+ "license": "MIT",
37
+ "homepage": "https://rundida.com",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/XWeaponX7/rundida-mcp"
41
+ },
42
+ "bugs": {
43
+ "url": "https://github.com/XWeaponX7/rundida-mcp/issues"
44
+ },
45
+ "dependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.0.0",
47
+ "zod": "^3.25 || ^4.0"
48
+ }
49
+ }