Skip to content

feat: Gamification MCP 'my progress' read tools - EXO-88399#1985

Open
bmestrallet wants to merge 2 commits into
feat/gamification-mcp-toolsfrom
feat/gamification-progress-reads
Open

feat: Gamification MCP 'my progress' read tools - EXO-88399#1985
bmestrallet wants to merge 2 commits into
feat/gamification-mcp-toolsfrom
feat/gamification-progress-reads

Conversation

@bmestrallet

Copy link
Copy Markdown

What

Follow-up batch of 6 read-only "my progress" tools on GamificationMcpTool, extending the campaigns/quests set. Campaign vocabulary; all reads, hard-scoped to the caller (no arbitrary identity param). Stacked on #1984.

Tool Backing
list_campaign_badges(campaign_id) BadgeService.findEnabledBadgesByProgramId (view-gated)
get_my_campaigns(limit, offset) ProgramService.getMemberProgramIds + getOwnedProgramIds
list_joinable_campaigns(limit, offset) ProgramService.getPublicProgramIds
get_my_points_total(from_date, to_date, campaign_id?) RealizationService.getScoreByIdentityIdAndBetweenDates
get_my_score_breakdown(period) RealizationService.getLeaderboardStatsByIdentityId
check_quest_availability(quest_id) getRealizationValidityContext + hasPendingRealization

list_my_realizations (already shipped) remains the per-event history; these add badges, my/joinable campaigns, windowed totals, breakdown, and the announce pre-check.

Tests & verification

  • mvn clean install -pl services -Pcoverage → BUILD SUCCESS, 354 tests, coverage gate 0.75 held; -parameters confirmed (MethodParameters 46).
  • Verified live with EVA (6/6 PASS): points total 211 = breakdown (181 + 30); quest availability returned a proper yes + reason.

AI contribution. Stacked on feat/gamification-mcp-tools (#1984).

@bmestrallet
bmestrallet requested a review from ahamdi July 6, 2026 16:49
@bmestrallet
bmestrallet force-pushed the feat/gamification-mcp-tools branch from 4d5446d to 3c68bf7 Compare July 19, 2026 11:01
@bmestrallet
bmestrallet force-pushed the feat/gamification-progress-reads branch from fa50b49 to 69b9b44 Compare July 19, 2026 11:03
@boubaker

Copy link
Copy Markdown
Member

Review: Gamification MCP 'my progress' read tools (stacked on #1984)

Overview

Adds 6 read-only tools to GamificationMcpTool: list_campaign_badges, get_my_campaigns, list_joinable_campaigns, get_my_points_total, get_my_score_breakdown, check_quest_availability. All hard-scoped to the current user (no arbitrary identity param) — good, avoids the class of ACL issue that mattered most in #1984. New DTOs are thin and consistent with the existing style.


🟠 Medium — get_my_campaigns can silently return up to 2x limit, and offset pagination over the union is unsound

GamificationMcpTool.java:337-350:

int clampedOffset = clampOffset(offset);
int clampedLimit = clampLimit(limit);
Set<Long> ids = new LinkedHashSet<>();
List<Long> memberIds = programService.getMemberProgramIds(currentUser, clampedOffset, clampedLimit);
List<Long> ownedIds = programService.getOwnedProgramIds(currentUser, clampedOffset, clampedLimit);
if (memberIds != null) ids.addAll(memberIds);
if (ownedIds != null) ids.addAll(ownedIds);
return resolveCampaigns(ids, currentUser);

limit/offset are each applied independently to two separate underlying id lists (member programs, owned programs), then the two pages are unioned with no limit re-applied to the merged result. Concretely:

  • If a user has ≥limit member programs and ≥limit owned programs with little/no overlap, the tool returns up to 2 × limit campaigns — silently violating the tool's own documented contract ("Maximum number of campaigns to return (default 10, max 50)").
  • Paging (offset > 0) is unsound over the combined set: each source list is paged independently, so there's no coherent "page 2 of my campaigns" — items can be skipped or duplicated relative to what a single combined, offset-limited query would produce.

None of the three getMyCampaigns* tests exercise a list length ≥ limit, so this isn't caught. Suggest fetching both id lists unbounded (or a safely large bound), dedup, sort deterministically, then apply offset/limit once to the deduplicated union — or push the limit down after building the full member∪owned id set.


Minor / style

  • checkQuestAvailability's buildAvailabilityReason cascades several RealizationValidityContext boolean flags — I worked through the boolean algebra against isValidForIdentity()/isValidButLocked()/isValidNoPrerequisites() and the reason cascade is logically sound (the "prerequisite" and "identity not eligible" branches correctly reduce to exactly the intended conditions). Nice, careful piece of logic.
  • resolveCampaignLabel falls back to a per-entry programService.getProgramById(...) call only when PiechartLeaderboard.getLabel() is blank, so the N+1 pattern only bites in the fallback path — acceptable given the typically small number of campaigns in a breakdown, but worth keeping an eye on if that list grows.
  • resolveCampaigns correctly re-checks ACL per id via getProgramById (unlike the bulk ProgramFilter/allSpaces path flagged in feat: Add Gamification MCP tools for EVA (campaigns & quests) - EXO-88395 #1984), and getMemberProgramIds/getOwnedProgramIds/getPublicProgramIds are pre-existing username-scoped service methods unrelated to that bug — this PR doesn't reintroduce the feat: Add Gamification MCP tools for EVA (campaigns & quests) - EXO-88395 #1984 ACL issue.
  • Same "declared throws IllegalAccessException that's actually always caught and translated internally" pattern from feat: Add Gamification MCP tools for EVA (campaigns & quests) - EXO-88395 #1984 continues here for listCampaignBadges/checkQuestAvailability (via resolveProgram/resolveRule) — not new, just flagging for consistency once feat: Add Gamification MCP tools for EVA (campaigns & quests) - EXO-88395 #1984's finding is addressed.

Test coverage

Good coverage of the new tools' happy/empty/error paths, including the dedup-across-member-and-owned case and the ACL-skip case for get_my_campaigns. As noted above, no test currently pins down the limit contract when the underlying lists are large, which is exactly the scenario that would expose the pagination bug.


🤖 This review was generated by Claude Code.

@bmestrallet

Copy link
Copy Markdown
Author

@boubaker — fixed (commit 4d53d5a): get_my_campaigns now fetches the full member and owned id sets unbounded, unions + dedups via a HashSet, sorts deterministically (ascending id), then applies offset/limit once over the union — so it can no longer return up to 2× limit and offset paging is sound. Test getMyCampaignsPaginatesOnceOverTheDedupedUnion: member=[1,2,3,4] ∪ owned=[4,5,6] → page(3,0)=[1,2,3], page(3,3)=[4,5,6], no dupes/overlap (pre-fix returned 6). Full suite 355 green.

bmestrallet and others added 2 commits July 20, 2026 13:18
Add 6 read-only campaign-progress tools to GamificationMcpTool, stacked on
the existing gamification MCP tools:
- list_campaign_badges: badges (levels) of a campaign + score needed
- get_my_campaigns: campaigns the user is a member of or owns
- list_joinable_campaigns: open public campaigns to join
- get_my_points_total: total points earned in a date range / campaign
- get_my_score_breakdown: points per campaign for a period
- check_quest_availability: pre-check for announce_quest with a reason

All tools are hard-scoped to the current user (self identity id / username
resolved via the existing helpers; no arbitrary identity accepted).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… review) - EXO-88399

get_my_campaigns applied limit/offset to the member-program and
owned-program id lists INDEPENDENTLY and then unioned them, so it could
return up to 2x limit items and offset paging over the union was unsound.

Fetch the full member and owned id sets (offset 0, unbounded limit -1),
union and dedup them, sort deterministically (ascending id), then apply
offset/limit ONCE over the sorted union. Add a regression test asserting
exactly `limit` items, no duplicates and correct offset paging.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@bmestrallet
bmestrallet force-pushed the feat/gamification-progress-reads branch from 4d53d5a to 3cee060 Compare July 20, 2026 12:20
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants