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.

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, not athleteId, in the request body.
  • Event priority should use atpPriority, not priority, 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 Gravel was 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:

  1. Compare upstream src/tp_mcp/tools/events.py to the local file.
  2. 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}
  3. 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
  4. 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:

  • RoadCycling
  • MountainBiking
  • Cyclocross
  • TrackCycling
  • Cycling

However, the actual TrainingPeaks event form uses web-form values such as:

  • CyclingRoad
  • CyclingMountain
  • CyclingCyclocross
  • CyclingTrack
  • CyclingOther

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: CyclingRoad
  • atpPriority: 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:

  1. Compare upstream EVENT_TYPES in src/tp_mcp/tools/events.py against the local file.
  2. Confirm upstream uses the web-form enum family, for example:
    • CyclingRoad
    • CyclingMountain
    • CyclingCyclocross
    • CyclingTrack
    • CyclingOther
  3. Confirm upstream tests were updated to the same enum family.
  4. Confirm the warning behavior is still desirable. If upstream handles readback validation differently but equivalently, local warning logic may be removable.
  5. 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:

File Patch 1 Patch 2 Notes
../trainingpeaks-mcp/src/tp_mcp/tools/events.py yes yes main implementation changes
../trainingpeaks-mcp/tests/test_tools/test_events.py yes yes test expectations updated
../trainingpeaks-mcp/PROGRESS.md yes yes local project tracking updated

Workspace records updated as a result

These workspace files were updated to track the outcome and durable rules:

  • athlete/calendar/2026-06-27.md
  • memory/2026-04-15.md
  • skills/trainingpeaks-activity-creator/SKILL.md
  • USER.md
  • MEMORY.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:

  1. Review upstream issue 55
  2. Review upstream issue 56
  3. Compare local patched files to upstream
  4. 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.