2026-04-15
TrainingPeaks MCP event patch record
Purpose
This document records the two local patches applied to the workspace copy of trainingpeaks-mcp on 2026-04-15.
The goal is to make the changes easy to review, easy to maintain, and easy to unroll later if the upstream repository fixes the same defects.
These patches were applied locally in:
../trainingpeaks-mcp/
They were not applied inside the workspace itself except for supporting memory and skill updates.
Related upstream issues
- Issue 55: https://github.com/JamsusMaximus/trainingpeaks-mcp/issues/55
- Issue 56: https://github.com/JamsusMaximus/trainingpeaks-mcp/issues/56
Patch 1, event create and delete route or payload fix
Problem summary
The local trainingpeaks-mcp event tools were using the wrong API route or payload fields for TrainingPeaks event creation and deletion.
Observed failures before patch:
- Event creation returned API errors when called through the MCP event path.
- Event deletion returned not found even for existing events.
Root cause found during direct API probing:
- Event create should use the singular route
/event, not/events. - Event create should send
personId, notathleteId, in the request body. - Event priority should use
atpPriority, notpriority, in the request body. - Event delete should use the singular route
/event/{event_id}, not/events/{event_id}.
Files changed
../trainingpeaks-mcp/src/tp_mcp/tools/events.py../trainingpeaks-mcp/tests/test_tools/test_events.py../trainingpeaks-mcp/PROGRESS.md
Exact code changes
File
../trainingpeaks-mcp/src/tp_mcp/tools/events.py
tp_create_event before
payload: dict[str, Any] = {
"athleteId": athlete_id,
"name": params.name,
"eventDate": f"{params.date}T00:00:00",
}
if params.event_type:
payload["eventType"] = params.event_type
if params.priority:
payload["priority"] = params.priority
if params.distance_km is not None:
payload["distance"] = params.distance_km * 1000
if params.ctl_target is not None:
payload["ctlTarget"] = params.ctl_target
if params.description:
payload["description"] = params.description
endpoint = f"/fitness/v6/athlete/{athlete_id}/events"
response = await client.post(endpoint, json=payload)
tp_create_event after
payload: dict[str, Any] = {
"personId": athlete_id,
"name": params.name,
"eventDate": f"{params.date}T00:00:00",
}
if params.event_type:
payload["eventType"] = params.event_type
if params.priority:
payload["atpPriority"] = params.priority
if params.distance_km is not None:
payload["distance"] = params.distance_km * 1000
if params.ctl_target is not None:
payload["ctlTarget"] = params.ctl_target
if params.description:
payload["description"] = params.description
endpoint = f"/fitness/v6/athlete/{athlete_id}/event"
response = await client.post(endpoint, json=payload)
tp_delete_event before
endpoint = f"/fitness/v6/athlete/{athlete_id}/events/{validated.workout_id}"
response = await client.delete(endpoint)
tp_delete_event after
endpoint = f"/fitness/v6/athlete/{athlete_id}/event/{validated.workout_id}"
response = await client.delete(endpoint)
File
../trainingpeaks-mcp/tests/test_tools/test_events.py
Test updates for patch 1
assert result["success"] is True
assert result["event_id"] == 501
endpoint = mock_instance.post.call_args[0][0]
payload = mock_instance.post.call_args[1]["json"]
assert endpoint == "/fitness/v6/athlete/123/event"
assert payload["personId"] == 123
assert payload["atpPriority"] == "A"
assert payload["distance"] == 226000.0
assert payload["ctlTarget"] == 120.0
assert result["success"] is True
endpoint = mock_instance.delete.call_args[0][0]
assert endpoint == "/fitness/v6/athlete/123/event/501"
Live verification performed
This patch was verified with direct live calls from the local trainingpeaks-mcp code.
Verified outcomes:
- test event creation succeeded
- test event deletion succeeded
Ghost of the Gravelwas successfully converted from a TrainingPeaks race workout fallback into a true TrainingPeaks event
Live artifact affected
- TrainingPeaks event:
Ghost of the Gravel - Final event ID after corrected creation:
38011994
Rollback guidance
If upstream fixes issue 55 and the fix is adopted locally, review the upstream implementation first.
To unroll this local patch safely:
- Compare upstream
src/tp_mcp/tools/events.pyto the local file. - Confirm upstream now uses:
- POST
/fitness/v6/athlete/{athlete_id}/event - body field
personId - body field
atpPriority - DELETE
/fitness/v6/athlete/{athlete_id}/event/{event_id}
- POST
- If upstream matches or improves on the local fix, revert the local custom edits in:
../trainingpeaks-mcp/src/tp_mcp/tools/events.py../trainingpeaks-mcp/tests/test_tools/test_events.py../trainingpeaks-mcp/PROGRESS.md
- Re-run a live create and delete test before considering the rollback complete.
Patch 2, event type enum mapping fix
Problem summary
The local trainingpeaks-mcp event type constants used the wrong enum family for TrainingPeaks event objects.
Before patch, the MCP used values such as:
RoadCyclingMountainBikingCyclocrossTrackCyclingCycling
However, the actual TrainingPeaks event form uses web-form values such as:
CyclingRoadCyclingMountainCyclingCyclocrossCyclingTrackCyclingOther
The HTML select provided during debugging confirmed the web UI values, and once those were used, the event type persisted correctly.
Files changed
../trainingpeaks-mcp/src/tp_mcp/tools/events.py../trainingpeaks-mcp/tests/test_tools/test_events.py../trainingpeaks-mcp/PROGRESS.md
Exact code changes
File
../trainingpeaks-mcp/src/tp_mcp/tools/events.py
EVENT_TYPES before
# Non-exhaustive list of known event types (TP API may accept others)
EVENT_TYPES = [
"RoadRunning", "TrailRunning", "TrackRunning", "CrossCountry", "Running",
"RoadCycling", "MountainBiking", "Cyclocross", "TrackCycling", "Cycling",
"OpenWaterSwimming", "PoolSwimming", "Triathlon", "MultisportTriathlon",
"Xterra", "Duathlon", "Aquabike", "Aquathon", "Multisport",
"Regatta", "Rowing",
"AlpineSkiing", "NordicSkiing", "SkiMountaineering", "Snowshoe", "Snow",
"Adventure", "Obstacle", "SpeedSkate", "Other",
]
EVENT_TYPES after
# Non-exhaustive list of known event types from the TrainingPeaks event UI.
# The API appears to accept these web-form enum values rather than the older
# humanized or mixed-case values that resemble activity type names.
EVENT_TYPES = [
"RunningRoad", "RunningTrail", "RunningTrack", "RunningCrossCountry", "RunningOther",
"CyclingRoad", "CyclingMountain", "CyclingCyclocross", "CyclingTrack", "CyclingOther",
"SwimOpenWater", "SwimPool",
"MultisportTriathlon", "MultisportXterra", "MultisportDuathlon",
"MultisportAquabike", "MultisportAquathon", "MultisportOther",
"RowingRegatta", "RowingOther",
"SnowAlpine", "SnowNordic", "SnowSkiMountaineering", "SnowSnowshoe", "SnowOther",
"OtherAdventure", "OtherObstacle", "OtherSpeedSkate", "OtherOther",
]
File
../trainingpeaks-mcp/tests/test_tools/test_events.py
Test updates for patch 2
result = await tp_create_event(
name="IRONMAN", date="2026-09-15",
event_type="Triathlon", priority="A",
distance_km=226.0, ctl_target=120.0,
)
became:
result = await tp_create_event(
name="IRONMAN", date="2026-09-15",
event_type="MultisportTriathlon", priority="A",
distance_km=226.0, ctl_target=120.0,
)
result = await tp_create_event(
name="Ghost", date="2026-06-27", event_type="RoadCycling", priority="C",
)
assert result["success"] is True
assert "warnings" in result
assert "Requested event type 'RoadCycling'" in result["warnings"][0]
became:
result = await tp_create_event(
name="Ghost", date="2026-06-27", event_type="CyclingRoad", priority="C",
)
assert result["success"] is True
assert "warnings" in result
assert "Requested event type 'CyclingRoad'" in result["warnings"][0]
Additional warning behavior added during debugging
While chasing the enum issue, local warning logic was also added so event create or update reports a mismatch if TrainingPeaks silently drops the requested type or priority.
File
../trainingpeaks-mcp/src/tp_mcp/tools/events.py
Added behavior
On create and update, when TrainingPeaks returns a different persisted value than requested, the result now includes warnings such as:
{
"success": True,
"message": "Event 38011994 updated.",
"warnings": [
"Requested event type 'RoadCycling', but TrainingPeaks returned 'Undefined'."
]
}
This warning behavior remains useful even after the enum fix because it protects against future silent normalization by the TP API.
Live verification performed
This patch was verified live against the Ghost of the Gravel event.
Verified final readback:
- event ID:
38011994 eventType: CyclingRoadatpPriority: C
This confirmed that the remaining issue was not the update route itself, but the incorrect event type enum values.
Rollback guidance
If upstream fixes issue 56 and the fix is adopted locally, review the upstream event type constants and tests first.
To unroll this local patch safely:
- Compare upstream
EVENT_TYPESinsrc/tp_mcp/tools/events.pyagainst the local file. - Confirm upstream uses the web-form enum family, for example:
CyclingRoadCyclingMountainCyclingCyclocrossCyclingTrackCyclingOther
- Confirm upstream tests were updated to the same enum family.
- Confirm the warning behavior is still desirable. If upstream handles readback validation differently but equivalently, local warning logic may be removable.
- Re-run a live event type update against a real event before considering the rollback complete.
Files changed across both patches
The following local repository files were modified across the two patches:
Workspace records updated as a result
These workspace files were updated to track the outcome and durable rules:
athlete/calendar/2026-06-27.mdmemory/2026-04-15.mdskills/trainingpeaks-activity-creator/SKILL.mdUSER.mdMEMORY.md
Operational guidance
Until upstream fixes from issues 55 and 56 are adopted locally, treat the local ../trainingpeaks-mcp/ tree as carrying required event-specific compatibility patches.
Before removing or replacing these local patches:
- Review upstream issue 55
- Review upstream issue 56
- Compare local patched files to upstream
- Re-run live TrainingPeaks validation for:
- event create
- event delete
- event update with
CyclingRoad - event priority update with
C
Do not unroll either patch only because the upstream issue is closed. Unroll only after live behavior matches the expected working behavior locally.