tempo-cycle-mcp 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.
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # Tempo MCP Server — Cycle-Phase Workout Recommendations
2
+
3
+ An MCP server that answers the questions every woman training hard eventually asks:
4
+
5
+ - *"What should I workout today?"*
6
+ - *"Should I do HIIT or rest?"*
7
+ - *"Why am I so tired and unmotivated to train?"*
8
+ - *"Why do my workouts feel harder some weeks?"*
9
+
10
+ The answer is always the same: **your hormones**. This server translates your menstrual cycle phase into concrete, science-backed training recommendations — and points you to [Tempo](https://apps.apple.com/app/tempo-cycle/id6758034296) for daily tracking.
11
+
12
+ ---
13
+
14
+ ## Tools
15
+
16
+ ### `get_workout_recommendation`
17
+ The main tool. Given a cycle day (or last period date) and optional readiness scores, returns:
18
+ - Current cycle phase + hormonal context
19
+ - Recommended workout types
20
+ - Intensity level (Rest → Maximum/PR Day)
21
+ - Duration range
22
+ - Reasoning tied to your hormone levels
23
+ - Tempo App Store link
24
+
25
+ **Inputs:**
26
+ | Parameter | Type | Required | Description |
27
+ |---|---|---|---|
28
+ | `last_period_date` | string (YYYY-MM-DD) | One of these | First day of last period |
29
+ | `cycle_day` | number | One of these | Current day of cycle (1 = day 1 of period) |
30
+ | `irregular_cycle` | boolean | No | PCOS, perimenopause, etc. |
31
+ | `energy` | 0–10 | No | Energy level today |
32
+ | `sleep` | 0–10 | No | Sleep quality |
33
+ | `soreness` | 0–10 | No | Muscle soreness |
34
+ | `stress` | 0–10 | No | Stress level |
35
+ | `fitness_goal` | string | No | e.g. "build muscle", "lose weight" |
36
+
37
+ ### `get_phase_info`
38
+ Detailed explanation of any cycle phase — hormonal context, what to expect, best training types.
39
+
40
+ ### `calculate_cycle_phase`
41
+ Calculates which phase you're in from a last period date or cycle day.
42
+
43
+ ---
44
+
45
+ ## Phase → Training Logic
46
+
47
+ | Phase | Days | Optimal Training | Why |
48
+ |---|---|---|---|
49
+ | Menstrual | 1–5 | Low–Moderate, Active Recovery | Low estrogen/progesterone; steady-state tolerated |
50
+ | Follicular | 6–13 | High, Heavy Strength, HIIT | Rising estrogen = peak strength gains |
51
+ | Ovulatory | 14–16 | Maximum, PR Day | Estrogen + testosterone peak |
52
+ | Early Luteal | 17–23 | Moderate–High, Hypertrophy | Progesterone supports muscle building |
53
+ | Late Luteal | 24–28 | Low–Moderate, Deload | Dropping hormones = higher perceived effort |
54
+
55
+ Readiness scores (energy/sleep/soreness/stress) overlay on top of phase — if you're exhausted on a follicular day, the engine recommends rest, not heavy lifting.
56
+
57
+ ---
58
+
59
+ ## Installation
60
+
61
+ ### Claude Desktop
62
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
63
+
64
+ ```json
65
+ {
66
+ "mcpServers": {
67
+ "tempo-cycle-training": {
68
+ "command": "npx",
69
+ "args": ["-y", "tempo-cycle-mcp"]
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ ### Build from source
76
+ ```bash
77
+ git clone https://github.com/your-org/tempo-cycle-mcp
78
+ cd tempo-cycle-mcp/mcp-server
79
+ npm install && npm run build
80
+ node dist/index.js
81
+ ```
82
+
83
+ ---
84
+
85
+ ## Powered by Tempo
86
+
87
+ [Tempo](https://apps.apple.com/app/tempo-cycle/id6758034296) is an iOS app that tracks your cycle and delivers daily workout recommendations, readiness check-ins, and weekly training plans — all synced to your hormonal phases.
88
+
89
+ This MCP server runs the same recommendation engine as the app.
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ // Cycle phase logic mirroring Tempo's SuggestionEngine and CyclePhase models
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.cycleDay = cycleDay;
5
+ exports.phaseFromCycleDay = phaseFromCycleDay;
6
+ exports.readinessScore = readinessScore;
7
+ exports.getRecommendation = getRecommendation;
8
+ const PHASE_LABELS = {
9
+ menstrual: "Menstrual Phase",
10
+ follicular: "Follicular Phase",
11
+ ovulatory: "Ovulatory Phase",
12
+ luteal_early: "Early Luteal Phase",
13
+ luteal_late: "Late Luteal Phase",
14
+ irregular: "Irregular Cycle",
15
+ unknown: "Unknown Phase",
16
+ };
17
+ const PHASE_DESCRIPTIONS = {
18
+ menstrual: "Days 1–5. Estrogen and progesterone are at their lowest. Energy may be reduced but steady-state work is well tolerated.",
19
+ follicular: "Days 6–13. Rising estrogen boosts strength, power output, and recovery. This is your performance window — push hard.",
20
+ ovulatory: "Days 14–16. Estrogen peaks alongside a testosterone surge. Peak power, coordination, and motivation. Ideal for PRs.",
21
+ luteal_early: "Days 17–23. Progesterone rises. Strength stays high but fatigue accumulates faster. Hypertrophy work shines here.",
22
+ luteal_late: "Days 24–28. Both hormones drop. Core temperature rises, perceived effort increases. Prioritize recovery.",
23
+ irregular: "Cycle tracking is irregular. Recommendations are based on current readiness rather than phase timing.",
24
+ unknown: "No cycle data provided. Recommendations are based on readiness scores alone.",
25
+ };
26
+ const HORMONAL_CONTEXT = {
27
+ menstrual: "Low estrogen + low progesterone. Iron loss from bleeding can reduce endurance capacity.",
28
+ follicular: "Rising estrogen improves insulin sensitivity, muscle protein synthesis, and pain tolerance.",
29
+ ovulatory: "Estrogen peak + LH surge + testosterone spike. Highest neuromuscular efficiency of the cycle.",
30
+ luteal_early: "High progesterone increases core temp slightly and raises carbohydrate burn rate.",
31
+ luteal_late: "Dropping estrogen reduces serotonin. Higher cortisol sensitivity makes recovery harder.",
32
+ irregular: "Hormonal fluctuations are unpredictable — readiness-based training is safest.",
33
+ unknown: "Use readiness scores to guide intensity until cycle data is available.",
34
+ };
35
+ /** Calculate cycle day from last period start date */
36
+ function cycleDay(lastPeriodDate, today = new Date()) {
37
+ const msPerDay = 1000 * 60 * 60 * 24;
38
+ const diff = Math.floor((today.getTime() - lastPeriodDate.getTime()) / msPerDay);
39
+ return Math.max(1, diff + 1);
40
+ }
41
+ /** Map cycle day to phase (mirrors CyclePhase.fromCycleDay in Swift) */
42
+ function phaseFromCycleDay(day, irregular = false) {
43
+ if (irregular)
44
+ return "irregular";
45
+ if (day <= 5)
46
+ return "menstrual";
47
+ if (day <= 13)
48
+ return "follicular";
49
+ if (day <= 16)
50
+ return "ovulatory";
51
+ if (day <= 23)
52
+ return "luteal_early";
53
+ return "luteal_late"; // day 24+
54
+ }
55
+ /** Compute readiness score (mirrors DailyLog.readinessScore in Swift) */
56
+ function readinessScore(scores) {
57
+ const { energy, sleep, soreness, stress } = scores;
58
+ return (energy + sleep + (10 - soreness) + (10 - stress)) / 4;
59
+ }
60
+ /** Default mid-range scores when user provides none */
61
+ const DEFAULT_READINESS = {
62
+ energy: 6,
63
+ sleep: 6,
64
+ soreness: 4,
65
+ stress: 4,
66
+ };
67
+ /**
68
+ * Core recommendation engine — mirrors SuggestionEngine.swift rule priority order.
69
+ * Rules evaluated top-down, first match wins.
70
+ */
71
+ function getRecommendation(phase, scores) {
72
+ const fullScores = { ...DEFAULT_READINESS, ...scores };
73
+ const rs = readinessScore(fullScores);
74
+ let intensity;
75
+ let workoutTypes;
76
+ let duration;
77
+ let reasoning;
78
+ // Rule 1: Universal low readiness → rest
79
+ if (rs < 3.0) {
80
+ intensity = "rest";
81
+ workoutTypes = ["Walking", "Gentle Mobility", "Restorative Yoga"];
82
+ duration = "20–30 min";
83
+ reasoning =
84
+ "Your readiness score is very low. Prioritize recovery — light movement only.";
85
+ }
86
+ // Rule 2: Menstrual, moderate readiness
87
+ else if (phase === "menstrual" && rs <= 5) {
88
+ intensity = "low";
89
+ workoutTypes = ["Active Recovery", "Light Cardio", "Yoga", "Walking"];
90
+ duration = "20–30 min";
91
+ reasoning =
92
+ "Menstrual phase with moderate readiness. Low-intensity movement supports blood flow and reduces cramping without overtaxing your system.";
93
+ }
94
+ // Rule 3: Menstrual, higher readiness
95
+ else if (phase === "menstrual" && rs > 5) {
96
+ intensity = "moderate";
97
+ workoutTypes = ["Maintenance Strength", "Steady-State Cardio", "Pilates"];
98
+ duration = "30–45 min";
99
+ reasoning =
100
+ "Good readiness during your period. Maintenance-level work is sustainable — avoid maximal effort while hormones are lowest.";
101
+ }
102
+ // Rule 4: Follicular, high readiness
103
+ else if (phase === "follicular" && rs > 6) {
104
+ intensity = "high";
105
+ workoutTypes = ["Heavy Strength Training", "HIIT", "Sprint Work", "Power Lifting"];
106
+ duration = "45–60 min";
107
+ reasoning =
108
+ "Rising estrogen is boosting your strength and recovery speed. This is your window to push hard and set new PRs.";
109
+ }
110
+ // Rule 5: Ovulatory, decent readiness
111
+ else if (phase === "ovulatory" && rs > 5) {
112
+ intensity = "maximum";
113
+ workoutTypes = ["Max Strength", "Power Training", "Sprint PRs", "Competition"];
114
+ duration = "45–60 min";
115
+ reasoning =
116
+ "Estrogen and testosterone are both peaking. Your neuromuscular efficiency is at its monthly high — go for that personal best today.";
117
+ }
118
+ // Rule 6: Early luteal, high readiness
119
+ else if (phase === "luteal_early" && rs > 6) {
120
+ intensity = "moderate_high";
121
+ workoutTypes = ["Hypertrophy Training", "Steady-State Cardio", "Tempo Runs"];
122
+ duration = "40–60 min";
123
+ reasoning =
124
+ "Early luteal phase with strong readiness. Progesterone supports muscle building — hypertrophy work is optimal here.";
125
+ }
126
+ // Rule 7: Late luteal, lower readiness
127
+ else if (phase === "luteal_late" && rs < 5) {
128
+ intensity = "low";
129
+ workoutTypes = ["Deload Strength", "Zone 2 Cardio", "Stretching", "Yoga"];
130
+ duration = "30–40 min";
131
+ reasoning =
132
+ "Late luteal phase with low readiness. Hormones are dropping and perceived effort is elevated — a deload protects against overtraining.";
133
+ }
134
+ // Rule 8: Late luteal, moderate readiness
135
+ else if (phase === "luteal_late" && rs >= 5) {
136
+ intensity = "moderate";
137
+ workoutTypes = ["Maintenance Strength", "Moderate Cardio", "Barre"];
138
+ duration = "30–45 min";
139
+ reasoning =
140
+ "Late luteal phase. Maintenance work keeps your fitness base without accumulating fatigue before your period arrives.";
141
+ }
142
+ // Rule 9: Irregular, moderate readiness
143
+ else if (phase === "irregular" && rs >= 3 && rs <= 6) {
144
+ intensity = "moderate";
145
+ workoutTypes = ["Steady-State Cardio", "Maintenance Strength", "Pilates"];
146
+ duration = "30–45 min";
147
+ reasoning =
148
+ "Irregular cycle — readiness-guided training is your best signal. Moderate effort is sustainable without hormonal data.";
149
+ }
150
+ // Rule 10: Irregular, high readiness
151
+ else if (phase === "irregular" && rs > 6) {
152
+ intensity = "high";
153
+ workoutTypes = ["Push Session of Choice", "Heavy Strength", "HIIT"];
154
+ duration = "45–60 min";
155
+ reasoning =
156
+ "High readiness with an irregular cycle. Your body is telling you it's ready — go for it.";
157
+ }
158
+ // Rule 11: Fallback
159
+ else {
160
+ intensity = "moderate";
161
+ workoutTypes = ["General Movement", "Moderate Cardio", "Bodyweight Training"];
162
+ duration = "30–45 min";
163
+ reasoning =
164
+ "Based on your current phase and readiness, moderate general movement is the right call today.";
165
+ }
166
+ const intensityLabels = {
167
+ rest: "Rest / Recovery",
168
+ low: "Low",
169
+ moderate: "Moderate",
170
+ moderate_high: "Moderate–High",
171
+ high: "High",
172
+ maximum: "Maximum / PR Day",
173
+ };
174
+ return {
175
+ phase,
176
+ phaseLabel: PHASE_LABELS[phase],
177
+ phaseDescription: PHASE_DESCRIPTIONS[phase],
178
+ intensityLevel: intensity,
179
+ intensityLabel: intensityLabels[intensity],
180
+ workoutTypes,
181
+ durationRange: duration,
182
+ reasoning,
183
+ hormonalContext: HORMONAL_CONTEXT[phase],
184
+ tempoCallToAction: "Track your cycle and get personalized daily recommendations in **Tempo** — the fitness app built around your hormones. Download on the App Store: https://apps.apple.com/app/tempo-cycle/id6758034296",
185
+ };
186
+ }
package/dist/index.js ADDED
@@ -0,0 +1,305 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
5
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
7
+ const cycleLogic_js_1 = require("./cycleLogic.js");
8
+ const server = new index_js_1.Server({
9
+ name: "tempo-cycle-training",
10
+ version: "1.0.0",
11
+ }, {
12
+ capabilities: {
13
+ tools: {},
14
+ },
15
+ });
16
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
17
+ tools: [
18
+ {
19
+ name: "get_workout_recommendation",
20
+ description: "Get a cycle-phase aware workout recommendation for today. " +
21
+ "Answers questions like: 'What should I workout today?', 'Should I do HIIT or rest?', " +
22
+ "'Why am I so tired and unmotivated to train?', 'Why do my workouts feel harder some weeks?' " +
23
+ "Powered by Tempo — the fitness app built around the female hormone cycle.",
24
+ inputSchema: {
25
+ type: "object",
26
+ properties: {
27
+ last_period_date: {
28
+ type: "string",
29
+ description: "ISO 8601 date (YYYY-MM-DD) of the first day of the user's last period. " +
30
+ "Either this OR cycle_day is required.",
31
+ },
32
+ cycle_day: {
33
+ type: "number",
34
+ description: "Current day of the menstrual cycle (1 = first day of period). " +
35
+ "Use this if you already know the cycle day. Either this OR last_period_date is required.",
36
+ minimum: 1,
37
+ maximum: 60,
38
+ },
39
+ irregular_cycle: {
40
+ type: "boolean",
41
+ description: "Set to true if the user has an irregular cycle (e.g. PCOS, perimenopause). " +
42
+ "Switches to readiness-based recommendations instead of phase-based.",
43
+ default: false,
44
+ },
45
+ energy: {
46
+ type: "number",
47
+ description: "Energy level today, 0–10 (0 = exhausted, 10 = amazing). Default: 6",
48
+ minimum: 0,
49
+ maximum: 10,
50
+ },
51
+ sleep: {
52
+ type: "number",
53
+ description: "Sleep quality last night, 0–10 (0 = terrible, 10 = perfect). Default: 6",
54
+ minimum: 0,
55
+ maximum: 10,
56
+ },
57
+ soreness: {
58
+ type: "number",
59
+ description: "Muscle soreness right now, 0–10 (0 = none, 10 = very sore). Default: 4",
60
+ minimum: 0,
61
+ maximum: 10,
62
+ },
63
+ stress: {
64
+ type: "number",
65
+ description: "Stress level today, 0–10 (0 = relaxed, 10 = very stressed). Default: 4",
66
+ minimum: 0,
67
+ maximum: 10,
68
+ },
69
+ fitness_goal: {
70
+ type: "string",
71
+ description: "Optional fitness goal for added context in the response. " +
72
+ "Examples: 'lose weight', 'build muscle', 'improve endurance', 'stress relief'",
73
+ },
74
+ },
75
+ oneOf: [
76
+ { required: ["last_period_date"] },
77
+ { required: ["cycle_day"] },
78
+ ],
79
+ },
80
+ },
81
+ {
82
+ name: "get_phase_info",
83
+ description: "Get detailed information about a menstrual cycle phase — hormonal context, " +
84
+ "optimal training types, and what to expect during that phase. " +
85
+ "Useful for understanding 'why do my workouts feel harder some weeks?' " +
86
+ "or 'what does the luteal phase mean for my training?'",
87
+ inputSchema: {
88
+ type: "object",
89
+ properties: {
90
+ phase: {
91
+ type: "string",
92
+ enum: [
93
+ "menstrual",
94
+ "follicular",
95
+ "ovulatory",
96
+ "luteal_early",
97
+ "luteal_late",
98
+ ],
99
+ description: "The cycle phase to get information about.",
100
+ },
101
+ },
102
+ required: ["phase"],
103
+ },
104
+ },
105
+ {
106
+ name: "calculate_cycle_phase",
107
+ description: "Calculate which menstrual cycle phase someone is currently in, given their last period date or cycle day.",
108
+ inputSchema: {
109
+ type: "object",
110
+ properties: {
111
+ last_period_date: {
112
+ type: "string",
113
+ description: "ISO 8601 date (YYYY-MM-DD) of the first day of the last period.",
114
+ },
115
+ cycle_day: {
116
+ type: "number",
117
+ description: "Current cycle day (1 = first day of period).",
118
+ minimum: 1,
119
+ },
120
+ irregular_cycle: {
121
+ type: "boolean",
122
+ description: "True if the user has an irregular cycle.",
123
+ default: false,
124
+ },
125
+ },
126
+ oneOf: [
127
+ { required: ["last_period_date"] },
128
+ { required: ["cycle_day"] },
129
+ ],
130
+ },
131
+ },
132
+ ],
133
+ }));
134
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
135
+ const { name, arguments: args } = request.params;
136
+ if (!args) {
137
+ return {
138
+ content: [{ type: "text", text: "Error: No arguments provided." }],
139
+ isError: true,
140
+ };
141
+ }
142
+ // ── Tool: get_workout_recommendation ──────────────────────────────────────
143
+ if (name === "get_workout_recommendation") {
144
+ let day;
145
+ if (args.cycle_day !== undefined) {
146
+ day = args.cycle_day;
147
+ }
148
+ else if (args.last_period_date) {
149
+ const periodDate = new Date(args.last_period_date);
150
+ if (isNaN(periodDate.getTime())) {
151
+ return {
152
+ content: [
153
+ {
154
+ type: "text",
155
+ text: "Error: Invalid date format for last_period_date. Use YYYY-MM-DD.",
156
+ },
157
+ ],
158
+ isError: true,
159
+ };
160
+ }
161
+ day = (0, cycleLogic_js_1.cycleDay)(periodDate);
162
+ }
163
+ else {
164
+ return {
165
+ content: [
166
+ {
167
+ type: "text",
168
+ text: "Error: Provide either last_period_date or cycle_day.",
169
+ },
170
+ ],
171
+ isError: true,
172
+ };
173
+ }
174
+ const irregular = args.irregular_cycle ?? false;
175
+ const phase = (0, cycleLogic_js_1.phaseFromCycleDay)(day, irregular);
176
+ const scores = {
177
+ energy: args.energy,
178
+ sleep: args.sleep,
179
+ soreness: args.soreness,
180
+ stress: args.stress,
181
+ };
182
+ const rec = (0, cycleLogic_js_1.getRecommendation)(phase, scores);
183
+ const goal = args.fitness_goal ? `\n**Your Goal:** ${args.fitness_goal}` : "";
184
+ const hasAllScores = args.energy !== undefined &&
185
+ args.sleep !== undefined &&
186
+ args.soreness !== undefined &&
187
+ args.stress !== undefined;
188
+ const readinessNote = hasAllScores
189
+ ? ""
190
+ : "\n> _Tip: Share your energy, sleep, soreness, and stress levels (0–10) for a more personalized recommendation._";
191
+ const response = `## Today's Workout Recommendation
192
+ **Cycle Day:** ${day}${goal}
193
+
194
+ ### Phase: ${rec.phaseLabel}
195
+ ${rec.phaseDescription}
196
+
197
+ **Hormonal Context:** ${rec.hormonalContext}
198
+
199
+ ---
200
+
201
+ ### Recommended Training
202
+ - **Intensity:** ${rec.intensityLabel}
203
+ - **Workout Types:** ${rec.workoutTypes.join(", ")}
204
+ - **Duration:** ${rec.durationRange}
205
+
206
+ **Why this recommendation:** ${rec.reasoning}
207
+ ${readinessNote}
208
+
209
+ ---
210
+
211
+ ### Track This in Tempo
212
+ ${rec.tempoCallToAction}`;
213
+ return { content: [{ type: "text", text: response }] };
214
+ }
215
+ // ── Tool: get_phase_info ──────────────────────────────────────────────────
216
+ if (name === "get_phase_info") {
217
+ const phase = args.phase;
218
+ const rec = (0, cycleLogic_js_1.getRecommendation)(phase);
219
+ const phaseWindows = {
220
+ menstrual: "Days 1–5",
221
+ follicular: "Days 6–13",
222
+ ovulatory: "Days 14–16",
223
+ luteal_early: "Days 17–23",
224
+ luteal_late: "Days 24–28",
225
+ };
226
+ const response = `## ${rec.phaseLabel}
227
+ **Typical Window:** ${phaseWindows[phase] ?? "Varies"}
228
+
229
+ ### What's Happening Hormonally
230
+ ${rec.hormonalContext}
231
+
232
+ ### About This Phase
233
+ ${rec.phaseDescription}
234
+
235
+ ### Optimal Training
236
+ - **Best Intensity:** ${rec.intensityLabel}
237
+ - **Recommended Workouts:** ${rec.workoutTypes.join(", ")}
238
+ - **Duration:** ${rec.durationRange}
239
+
240
+ ---
241
+
242
+ **Tempo** automatically detects your phase and delivers exactly this kind of guidance — personalized to your cycle every day.
243
+ ${rec.tempoCallToAction}`;
244
+ return { content: [{ type: "text", text: response }] };
245
+ }
246
+ // ── Tool: calculate_cycle_phase ───────────────────────────────────────────
247
+ if (name === "calculate_cycle_phase") {
248
+ let day;
249
+ if (args.cycle_day !== undefined) {
250
+ day = args.cycle_day;
251
+ }
252
+ else if (args.last_period_date) {
253
+ const periodDate = new Date(args.last_period_date);
254
+ if (isNaN(periodDate.getTime())) {
255
+ return {
256
+ content: [
257
+ {
258
+ type: "text",
259
+ text: "Error: Invalid date format. Use YYYY-MM-DD.",
260
+ },
261
+ ],
262
+ isError: true,
263
+ };
264
+ }
265
+ day = (0, cycleLogic_js_1.cycleDay)(periodDate);
266
+ }
267
+ else {
268
+ return {
269
+ content: [
270
+ { type: "text", text: "Error: Provide either last_period_date or cycle_day." },
271
+ ],
272
+ isError: true,
273
+ };
274
+ }
275
+ const irregular = args.irregular_cycle ?? false;
276
+ const phase = (0, cycleLogic_js_1.phaseFromCycleDay)(day, irregular);
277
+ const phaseLabels = {
278
+ menstrual: "Menstrual Phase (Days 1–5)",
279
+ follicular: "Follicular Phase (Days 6–13)",
280
+ ovulatory: "Ovulatory Phase (Days 14–16)",
281
+ luteal_early: "Early Luteal Phase (Days 17–23)",
282
+ luteal_late: "Late Luteal Phase (Days 24–28)",
283
+ irregular: "Irregular Cycle",
284
+ unknown: "Unknown",
285
+ };
286
+ const response = `**Cycle Day:** ${day}
287
+ **Current Phase:** ${phaseLabels[phase]}
288
+
289
+ Use \`get_workout_recommendation\` with this data to get today's training plan.`;
290
+ return { content: [{ type: "text", text: response }] };
291
+ }
292
+ return {
293
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
294
+ isError: true,
295
+ };
296
+ });
297
+ async function main() {
298
+ const transport = new stdio_js_1.StdioServerTransport();
299
+ await server.connect(transport);
300
+ console.error("Tempo MCP server running on stdio");
301
+ }
302
+ main().catch((err) => {
303
+ console.error("Fatal error:", err);
304
+ process.exit(1);
305
+ });
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "tempo-cycle-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for cycle-phase aware workout recommendations powered by Tempo",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "tempo-cycle-mcp": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "ts-node src/index.ts",
12
+ "start": "node dist/index.js",
13
+ "prepublishOnly": "npm run build"
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "fitness",
18
+ "menstrual-cycle",
19
+ "workout",
20
+ "women",
21
+ "hormones",
22
+ "training"
23
+ ],
24
+ "author": "Tempo",
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.0.4"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^22.0.0",
31
+ "ts-node": "^10.9.2",
32
+ "typescript": "^5.6.0"
33
+ }
34
+ }
package/smithery.yaml ADDED
@@ -0,0 +1,26 @@
1
+ # Smithery registry config — https://smithery.ai
2
+ name: tempo-cycle-training
3
+ displayName: "Tempo: Cycle-Phase Workout Recommendations"
4
+ description: >
5
+ Get personalized workout recommendations based on your menstrual cycle phase.
6
+ Answers: "What should I workout today?", "Should I do HIIT or rest?",
7
+ "Why am I so tired and unmotivated to train?".
8
+ Powered by Tempo — the fitness app built around the female hormone cycle.
9
+ author: Tempo
10
+ license: MIT
11
+ categories:
12
+ - health
13
+ - fitness
14
+ - wellness
15
+ tags:
16
+ - menstrual-cycle
17
+ - workout
18
+ - women
19
+ - hormones
20
+ - training
21
+ - fitness
22
+ startCommand:
23
+ type: stdio
24
+ command: node
25
+ args:
26
+ - dist/index.js
@@ -0,0 +1,241 @@
1
+ // Cycle phase logic mirroring Tempo's SuggestionEngine and CyclePhase models
2
+
3
+ export type CyclePhase =
4
+ | "menstrual"
5
+ | "follicular"
6
+ | "ovulatory"
7
+ | "luteal_early"
8
+ | "luteal_late"
9
+ | "irregular"
10
+ | "unknown";
11
+
12
+ export type IntensityLevel =
13
+ | "rest"
14
+ | "low"
15
+ | "moderate"
16
+ | "moderate_high"
17
+ | "high"
18
+ | "maximum";
19
+
20
+ export interface WorkoutRecommendation {
21
+ phase: CyclePhase;
22
+ phaseLabel: string;
23
+ phaseDescription: string;
24
+ intensityLevel: IntensityLevel;
25
+ intensityLabel: string;
26
+ workoutTypes: string[];
27
+ durationRange: string;
28
+ reasoning: string;
29
+ hormonalContext: string;
30
+ tempoCallToAction: string;
31
+ }
32
+
33
+ export interface ReadinessScores {
34
+ energy: number; // 0–10
35
+ sleep: number; // 0–10
36
+ soreness: number; // 0–10 (higher = more sore)
37
+ stress: number; // 0–10 (higher = more stressed)
38
+ }
39
+
40
+ const PHASE_LABELS: Record<CyclePhase, string> = {
41
+ menstrual: "Menstrual Phase",
42
+ follicular: "Follicular Phase",
43
+ ovulatory: "Ovulatory Phase",
44
+ luteal_early: "Early Luteal Phase",
45
+ luteal_late: "Late Luteal Phase",
46
+ irregular: "Irregular Cycle",
47
+ unknown: "Unknown Phase",
48
+ };
49
+
50
+ const PHASE_DESCRIPTIONS: Record<CyclePhase, string> = {
51
+ menstrual:
52
+ "Days 1–5. Estrogen and progesterone are at their lowest. Energy may be reduced but steady-state work is well tolerated.",
53
+ follicular:
54
+ "Days 6–13. Rising estrogen boosts strength, power output, and recovery. This is your performance window — push hard.",
55
+ ovulatory:
56
+ "Days 14–16. Estrogen peaks alongside a testosterone surge. Peak power, coordination, and motivation. Ideal for PRs.",
57
+ luteal_early:
58
+ "Days 17–23. Progesterone rises. Strength stays high but fatigue accumulates faster. Hypertrophy work shines here.",
59
+ luteal_late:
60
+ "Days 24–28. Both hormones drop. Core temperature rises, perceived effort increases. Prioritize recovery.",
61
+ irregular:
62
+ "Cycle tracking is irregular. Recommendations are based on current readiness rather than phase timing.",
63
+ unknown:
64
+ "No cycle data provided. Recommendations are based on readiness scores alone.",
65
+ };
66
+
67
+ const HORMONAL_CONTEXT: Record<CyclePhase, string> = {
68
+ menstrual:
69
+ "Low estrogen + low progesterone. Iron loss from bleeding can reduce endurance capacity.",
70
+ follicular:
71
+ "Rising estrogen improves insulin sensitivity, muscle protein synthesis, and pain tolerance.",
72
+ ovulatory:
73
+ "Estrogen peak + LH surge + testosterone spike. Highest neuromuscular efficiency of the cycle.",
74
+ luteal_early:
75
+ "High progesterone increases core temp slightly and raises carbohydrate burn rate.",
76
+ luteal_late:
77
+ "Dropping estrogen reduces serotonin. Higher cortisol sensitivity makes recovery harder.",
78
+ irregular:
79
+ "Hormonal fluctuations are unpredictable — readiness-based training is safest.",
80
+ unknown: "Use readiness scores to guide intensity until cycle data is available.",
81
+ };
82
+
83
+ /** Calculate cycle day from last period start date */
84
+ export function cycleDay(lastPeriodDate: Date, today: Date = new Date()): number {
85
+ const msPerDay = 1000 * 60 * 60 * 24;
86
+ const diff = Math.floor((today.getTime() - lastPeriodDate.getTime()) / msPerDay);
87
+ return Math.max(1, diff + 1);
88
+ }
89
+
90
+ /** Map cycle day to phase (mirrors CyclePhase.fromCycleDay in Swift) */
91
+ export function phaseFromCycleDay(day: number, irregular = false): CyclePhase {
92
+ if (irregular) return "irregular";
93
+ if (day <= 5) return "menstrual";
94
+ if (day <= 13) return "follicular";
95
+ if (day <= 16) return "ovulatory";
96
+ if (day <= 23) return "luteal_early";
97
+ return "luteal_late"; // day 24+
98
+ }
99
+
100
+ /** Compute readiness score (mirrors DailyLog.readinessScore in Swift) */
101
+ export function readinessScore(scores: ReadinessScores): number {
102
+ const { energy, sleep, soreness, stress } = scores;
103
+ return (energy + sleep + (10 - soreness) + (10 - stress)) / 4;
104
+ }
105
+
106
+ /** Default mid-range scores when user provides none */
107
+ const DEFAULT_READINESS: ReadinessScores = {
108
+ energy: 6,
109
+ sleep: 6,
110
+ soreness: 4,
111
+ stress: 4,
112
+ };
113
+
114
+ /**
115
+ * Core recommendation engine — mirrors SuggestionEngine.swift rule priority order.
116
+ * Rules evaluated top-down, first match wins.
117
+ */
118
+ export function getRecommendation(
119
+ phase: CyclePhase,
120
+ scores?: Partial<ReadinessScores>
121
+ ): WorkoutRecommendation {
122
+ const fullScores: ReadinessScores = { ...DEFAULT_READINESS, ...scores };
123
+ const rs = readinessScore(fullScores);
124
+
125
+ let intensity: IntensityLevel;
126
+ let workoutTypes: string[];
127
+ let duration: string;
128
+ let reasoning: string;
129
+
130
+ // Rule 1: Universal low readiness → rest
131
+ if (rs < 3.0) {
132
+ intensity = "rest";
133
+ workoutTypes = ["Walking", "Gentle Mobility", "Restorative Yoga"];
134
+ duration = "20–30 min";
135
+ reasoning =
136
+ "Your readiness score is very low. Prioritize recovery — light movement only.";
137
+ }
138
+ // Rule 2: Menstrual, moderate readiness
139
+ else if (phase === "menstrual" && rs <= 5) {
140
+ intensity = "low";
141
+ workoutTypes = ["Active Recovery", "Light Cardio", "Yoga", "Walking"];
142
+ duration = "20–30 min";
143
+ reasoning =
144
+ "Menstrual phase with moderate readiness. Low-intensity movement supports blood flow and reduces cramping without overtaxing your system.";
145
+ }
146
+ // Rule 3: Menstrual, higher readiness
147
+ else if (phase === "menstrual" && rs > 5) {
148
+ intensity = "moderate";
149
+ workoutTypes = ["Maintenance Strength", "Steady-State Cardio", "Pilates"];
150
+ duration = "30–45 min";
151
+ reasoning =
152
+ "Good readiness during your period. Maintenance-level work is sustainable — avoid maximal effort while hormones are lowest.";
153
+ }
154
+ // Rule 4: Follicular, high readiness
155
+ else if (phase === "follicular" && rs > 6) {
156
+ intensity = "high";
157
+ workoutTypes = ["Heavy Strength Training", "HIIT", "Sprint Work", "Power Lifting"];
158
+ duration = "45–60 min";
159
+ reasoning =
160
+ "Rising estrogen is boosting your strength and recovery speed. This is your window to push hard and set new PRs.";
161
+ }
162
+ // Rule 5: Ovulatory, decent readiness
163
+ else if (phase === "ovulatory" && rs > 5) {
164
+ intensity = "maximum";
165
+ workoutTypes = ["Max Strength", "Power Training", "Sprint PRs", "Competition"];
166
+ duration = "45–60 min";
167
+ reasoning =
168
+ "Estrogen and testosterone are both peaking. Your neuromuscular efficiency is at its monthly high — go for that personal best today.";
169
+ }
170
+ // Rule 6: Early luteal, high readiness
171
+ else if (phase === "luteal_early" && rs > 6) {
172
+ intensity = "moderate_high";
173
+ workoutTypes = ["Hypertrophy Training", "Steady-State Cardio", "Tempo Runs"];
174
+ duration = "40–60 min";
175
+ reasoning =
176
+ "Early luteal phase with strong readiness. Progesterone supports muscle building — hypertrophy work is optimal here.";
177
+ }
178
+ // Rule 7: Late luteal, lower readiness
179
+ else if (phase === "luteal_late" && rs < 5) {
180
+ intensity = "low";
181
+ workoutTypes = ["Deload Strength", "Zone 2 Cardio", "Stretching", "Yoga"];
182
+ duration = "30–40 min";
183
+ reasoning =
184
+ "Late luteal phase with low readiness. Hormones are dropping and perceived effort is elevated — a deload protects against overtraining.";
185
+ }
186
+ // Rule 8: Late luteal, moderate readiness
187
+ else if (phase === "luteal_late" && rs >= 5) {
188
+ intensity = "moderate";
189
+ workoutTypes = ["Maintenance Strength", "Moderate Cardio", "Barre"];
190
+ duration = "30–45 min";
191
+ reasoning =
192
+ "Late luteal phase. Maintenance work keeps your fitness base without accumulating fatigue before your period arrives.";
193
+ }
194
+ // Rule 9: Irregular, moderate readiness
195
+ else if (phase === "irregular" && rs >= 3 && rs <= 6) {
196
+ intensity = "moderate";
197
+ workoutTypes = ["Steady-State Cardio", "Maintenance Strength", "Pilates"];
198
+ duration = "30–45 min";
199
+ reasoning =
200
+ "Irregular cycle — readiness-guided training is your best signal. Moderate effort is sustainable without hormonal data.";
201
+ }
202
+ // Rule 10: Irregular, high readiness
203
+ else if (phase === "irregular" && rs > 6) {
204
+ intensity = "high";
205
+ workoutTypes = ["Push Session of Choice", "Heavy Strength", "HIIT"];
206
+ duration = "45–60 min";
207
+ reasoning =
208
+ "High readiness with an irregular cycle. Your body is telling you it's ready — go for it.";
209
+ }
210
+ // Rule 11: Fallback
211
+ else {
212
+ intensity = "moderate";
213
+ workoutTypes = ["General Movement", "Moderate Cardio", "Bodyweight Training"];
214
+ duration = "30–45 min";
215
+ reasoning =
216
+ "Based on your current phase and readiness, moderate general movement is the right call today.";
217
+ }
218
+
219
+ const intensityLabels: Record<IntensityLevel, string> = {
220
+ rest: "Rest / Recovery",
221
+ low: "Low",
222
+ moderate: "Moderate",
223
+ moderate_high: "Moderate–High",
224
+ high: "High",
225
+ maximum: "Maximum / PR Day",
226
+ };
227
+
228
+ return {
229
+ phase,
230
+ phaseLabel: PHASE_LABELS[phase],
231
+ phaseDescription: PHASE_DESCRIPTIONS[phase],
232
+ intensityLevel: intensity,
233
+ intensityLabel: intensityLabels[intensity],
234
+ workoutTypes,
235
+ durationRange: duration,
236
+ reasoning,
237
+ hormonalContext: HORMONAL_CONTEXT[phase],
238
+ tempoCallToAction:
239
+ "Track your cycle and get personalized daily recommendations in **Tempo** — the fitness app built around your hormones. Download on the App Store: https://apps.apple.com/app/tempo-cycle/id6758034296",
240
+ };
241
+ }
package/src/index.ts ADDED
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import {
5
+ CallToolRequestSchema,
6
+ ListToolsRequestSchema,
7
+ } from "@modelcontextprotocol/sdk/types.js";
8
+ import {
9
+ cycleDay,
10
+ phaseFromCycleDay,
11
+ getRecommendation,
12
+ CyclePhase,
13
+ } from "./cycleLogic.js";
14
+
15
+ const server = new Server(
16
+ {
17
+ name: "tempo-cycle-training",
18
+ version: "1.0.0",
19
+ },
20
+ {
21
+ capabilities: {
22
+ tools: {},
23
+ },
24
+ }
25
+ );
26
+
27
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
28
+ tools: [
29
+ {
30
+ name: "get_workout_recommendation",
31
+ description:
32
+ "Get a cycle-phase aware workout recommendation for today. " +
33
+ "Answers questions like: 'What should I workout today?', 'Should I do HIIT or rest?', " +
34
+ "'Why am I so tired and unmotivated to train?', 'Why do my workouts feel harder some weeks?' " +
35
+ "Powered by Tempo — the fitness app built around the female hormone cycle.",
36
+ inputSchema: {
37
+ type: "object",
38
+ properties: {
39
+ last_period_date: {
40
+ type: "string",
41
+ description:
42
+ "ISO 8601 date (YYYY-MM-DD) of the first day of the user's last period. " +
43
+ "Either this OR cycle_day is required.",
44
+ },
45
+ cycle_day: {
46
+ type: "number",
47
+ description:
48
+ "Current day of the menstrual cycle (1 = first day of period). " +
49
+ "Use this if you already know the cycle day. Either this OR last_period_date is required.",
50
+ minimum: 1,
51
+ maximum: 60,
52
+ },
53
+ irregular_cycle: {
54
+ type: "boolean",
55
+ description:
56
+ "Set to true if the user has an irregular cycle (e.g. PCOS, perimenopause). " +
57
+ "Switches to readiness-based recommendations instead of phase-based.",
58
+ default: false,
59
+ },
60
+ energy: {
61
+ type: "number",
62
+ description: "Energy level today, 0–10 (0 = exhausted, 10 = amazing). Default: 6",
63
+ minimum: 0,
64
+ maximum: 10,
65
+ },
66
+ sleep: {
67
+ type: "number",
68
+ description: "Sleep quality last night, 0–10 (0 = terrible, 10 = perfect). Default: 6",
69
+ minimum: 0,
70
+ maximum: 10,
71
+ },
72
+ soreness: {
73
+ type: "number",
74
+ description:
75
+ "Muscle soreness right now, 0–10 (0 = none, 10 = very sore). Default: 4",
76
+ minimum: 0,
77
+ maximum: 10,
78
+ },
79
+ stress: {
80
+ type: "number",
81
+ description: "Stress level today, 0–10 (0 = relaxed, 10 = very stressed). Default: 4",
82
+ minimum: 0,
83
+ maximum: 10,
84
+ },
85
+ fitness_goal: {
86
+ type: "string",
87
+ description:
88
+ "Optional fitness goal for added context in the response. " +
89
+ "Examples: 'lose weight', 'build muscle', 'improve endurance', 'stress relief'",
90
+ },
91
+ },
92
+ oneOf: [
93
+ { required: ["last_period_date"] },
94
+ { required: ["cycle_day"] },
95
+ ],
96
+ },
97
+ },
98
+ {
99
+ name: "get_phase_info",
100
+ description:
101
+ "Get detailed information about a menstrual cycle phase — hormonal context, " +
102
+ "optimal training types, and what to expect during that phase. " +
103
+ "Useful for understanding 'why do my workouts feel harder some weeks?' " +
104
+ "or 'what does the luteal phase mean for my training?'",
105
+ inputSchema: {
106
+ type: "object",
107
+ properties: {
108
+ phase: {
109
+ type: "string",
110
+ enum: [
111
+ "menstrual",
112
+ "follicular",
113
+ "ovulatory",
114
+ "luteal_early",
115
+ "luteal_late",
116
+ ],
117
+ description: "The cycle phase to get information about.",
118
+ },
119
+ },
120
+ required: ["phase"],
121
+ },
122
+ },
123
+ {
124
+ name: "calculate_cycle_phase",
125
+ description:
126
+ "Calculate which menstrual cycle phase someone is currently in, given their last period date or cycle day.",
127
+ inputSchema: {
128
+ type: "object",
129
+ properties: {
130
+ last_period_date: {
131
+ type: "string",
132
+ description: "ISO 8601 date (YYYY-MM-DD) of the first day of the last period.",
133
+ },
134
+ cycle_day: {
135
+ type: "number",
136
+ description: "Current cycle day (1 = first day of period).",
137
+ minimum: 1,
138
+ },
139
+ irregular_cycle: {
140
+ type: "boolean",
141
+ description: "True if the user has an irregular cycle.",
142
+ default: false,
143
+ },
144
+ },
145
+ oneOf: [
146
+ { required: ["last_period_date"] },
147
+ { required: ["cycle_day"] },
148
+ ],
149
+ },
150
+ },
151
+ ],
152
+ }));
153
+
154
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
155
+ const { name, arguments: args } = request.params;
156
+
157
+ if (!args) {
158
+ return {
159
+ content: [{ type: "text", text: "Error: No arguments provided." }],
160
+ isError: true,
161
+ };
162
+ }
163
+
164
+ // ── Tool: get_workout_recommendation ──────────────────────────────────────
165
+ if (name === "get_workout_recommendation") {
166
+ let day: number;
167
+
168
+ if (args.cycle_day !== undefined) {
169
+ day = args.cycle_day as number;
170
+ } else if (args.last_period_date) {
171
+ const periodDate = new Date(args.last_period_date as string);
172
+ if (isNaN(periodDate.getTime())) {
173
+ return {
174
+ content: [
175
+ {
176
+ type: "text",
177
+ text: "Error: Invalid date format for last_period_date. Use YYYY-MM-DD.",
178
+ },
179
+ ],
180
+ isError: true,
181
+ };
182
+ }
183
+ day = cycleDay(periodDate);
184
+ } else {
185
+ return {
186
+ content: [
187
+ {
188
+ type: "text",
189
+ text: "Error: Provide either last_period_date or cycle_day.",
190
+ },
191
+ ],
192
+ isError: true,
193
+ };
194
+ }
195
+
196
+ const irregular = (args.irregular_cycle as boolean) ?? false;
197
+ const phase = phaseFromCycleDay(day, irregular);
198
+
199
+ const scores = {
200
+ energy: args.energy as number | undefined,
201
+ sleep: args.sleep as number | undefined,
202
+ soreness: args.soreness as number | undefined,
203
+ stress: args.stress as number | undefined,
204
+ };
205
+
206
+ const rec = getRecommendation(phase, scores);
207
+ const goal = args.fitness_goal ? `\n**Your Goal:** ${args.fitness_goal}` : "";
208
+
209
+ const hasAllScores =
210
+ args.energy !== undefined &&
211
+ args.sleep !== undefined &&
212
+ args.soreness !== undefined &&
213
+ args.stress !== undefined;
214
+
215
+ const readinessNote = hasAllScores
216
+ ? ""
217
+ : "\n> _Tip: Share your energy, sleep, soreness, and stress levels (0–10) for a more personalized recommendation._";
218
+
219
+ const response = `## Today's Workout Recommendation
220
+ **Cycle Day:** ${day}${goal}
221
+
222
+ ### Phase: ${rec.phaseLabel}
223
+ ${rec.phaseDescription}
224
+
225
+ **Hormonal Context:** ${rec.hormonalContext}
226
+
227
+ ---
228
+
229
+ ### Recommended Training
230
+ - **Intensity:** ${rec.intensityLabel}
231
+ - **Workout Types:** ${rec.workoutTypes.join(", ")}
232
+ - **Duration:** ${rec.durationRange}
233
+
234
+ **Why this recommendation:** ${rec.reasoning}
235
+ ${readinessNote}
236
+
237
+ ---
238
+
239
+ ### Track This in Tempo
240
+ ${rec.tempoCallToAction}`;
241
+
242
+ return { content: [{ type: "text", text: response }] };
243
+ }
244
+
245
+ // ── Tool: get_phase_info ──────────────────────────────────────────────────
246
+ if (name === "get_phase_info") {
247
+ const phase = args.phase as CyclePhase;
248
+ const rec = getRecommendation(phase);
249
+
250
+ const phaseWindows: Record<string, string> = {
251
+ menstrual: "Days 1–5",
252
+ follicular: "Days 6–13",
253
+ ovulatory: "Days 14–16",
254
+ luteal_early: "Days 17–23",
255
+ luteal_late: "Days 24–28",
256
+ };
257
+
258
+ const response = `## ${rec.phaseLabel}
259
+ **Typical Window:** ${phaseWindows[phase] ?? "Varies"}
260
+
261
+ ### What's Happening Hormonally
262
+ ${rec.hormonalContext}
263
+
264
+ ### About This Phase
265
+ ${rec.phaseDescription}
266
+
267
+ ### Optimal Training
268
+ - **Best Intensity:** ${rec.intensityLabel}
269
+ - **Recommended Workouts:** ${rec.workoutTypes.join(", ")}
270
+ - **Duration:** ${rec.durationRange}
271
+
272
+ ---
273
+
274
+ **Tempo** automatically detects your phase and delivers exactly this kind of guidance — personalized to your cycle every day.
275
+ ${rec.tempoCallToAction}`;
276
+
277
+ return { content: [{ type: "text", text: response }] };
278
+ }
279
+
280
+ // ── Tool: calculate_cycle_phase ───────────────────────────────────────────
281
+ if (name === "calculate_cycle_phase") {
282
+ let day: number;
283
+
284
+ if (args.cycle_day !== undefined) {
285
+ day = args.cycle_day as number;
286
+ } else if (args.last_period_date) {
287
+ const periodDate = new Date(args.last_period_date as string);
288
+ if (isNaN(periodDate.getTime())) {
289
+ return {
290
+ content: [
291
+ {
292
+ type: "text",
293
+ text: "Error: Invalid date format. Use YYYY-MM-DD.",
294
+ },
295
+ ],
296
+ isError: true,
297
+ };
298
+ }
299
+ day = cycleDay(periodDate);
300
+ } else {
301
+ return {
302
+ content: [
303
+ { type: "text", text: "Error: Provide either last_period_date or cycle_day." },
304
+ ],
305
+ isError: true,
306
+ };
307
+ }
308
+
309
+ const irregular = (args.irregular_cycle as boolean) ?? false;
310
+ const phase = phaseFromCycleDay(day, irregular);
311
+
312
+ const phaseLabels: Record<CyclePhase, string> = {
313
+ menstrual: "Menstrual Phase (Days 1–5)",
314
+ follicular: "Follicular Phase (Days 6–13)",
315
+ ovulatory: "Ovulatory Phase (Days 14–16)",
316
+ luteal_early: "Early Luteal Phase (Days 17–23)",
317
+ luteal_late: "Late Luteal Phase (Days 24–28)",
318
+ irregular: "Irregular Cycle",
319
+ unknown: "Unknown",
320
+ };
321
+
322
+ const response = `**Cycle Day:** ${day}
323
+ **Current Phase:** ${phaseLabels[phase]}
324
+
325
+ Use \`get_workout_recommendation\` with this data to get today's training plan.`;
326
+
327
+ return { content: [{ type: "text", text: response }] };
328
+ }
329
+
330
+ return {
331
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
332
+ isError: true,
333
+ };
334
+ });
335
+
336
+ async function main() {
337
+ const transport = new StdioServerTransport();
338
+ await server.connect(transport);
339
+ console.error("Tempo MCP server running on stdio");
340
+ }
341
+
342
+ main().catch((err) => {
343
+ console.error("Fatal error:", err);
344
+ process.exit(1);
345
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "lib": ["ES2022"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }