cito-mcp 0.4.5 → 0.4.6

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 CHANGED
@@ -535,7 +535,7 @@ claude mcp add cito -e CITO_API_KEY=cito_… "--" npx -y cito-mcp
535
535
  ### Semver expectations
536
536
 
537
537
  - **0.2.4** is a **major surface break** vs 0.1 (tool rename + removal of OpenAPI mass-generation).
538
- - Further 0.2.x patches may refine envelopes and composite quality without renaming the 15 tools.
538
+ - Further 0.2.x patches may refine envelopes and composite quality without renaming the shipped tools. (This line previously said "15 tools" while the catalog shipped 42 — the count here is intentionally not restated, because a number duplicated in prose drifts the moment the catalog changes.)
539
539
  - Promoting Fortnite (or other titles) into the primary `game` enum would be a minor feature bump with catalog/docs updates.
540
540
 
541
541
  ---
package/dist/client.js CHANGED
@@ -102,6 +102,8 @@ export async function fetchJson(ctx, path, opts) {
102
102
  catch {
103
103
  data = text;
104
104
  }
105
+ if (!opts?.raw && response.ok && isTennisPath(path))
106
+ data = flattenTennisEnvelope(data);
105
107
  return {
106
108
  ok: response.ok,
107
109
  status: response.status,
@@ -158,6 +160,43 @@ export function asRecord(value) {
158
160
  }
159
161
  return null;
160
162
  }
163
+ const TENNIS_LIST_META_KEYS = new Set(['count', 'limit', 'total', 'page', 'totalPages', 'hasNext', 'hasPrev']);
164
+ function isTennisPath(path) {
165
+ return /^(\/api\/v1)?\/tennis(\/|\?|$)/.test(path);
166
+ }
167
+ function snakeKey(key) {
168
+ return key.replace(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`);
169
+ }
170
+ export function flattenTennisEnvelope(body) {
171
+ const env = asRecord(body);
172
+ if (!env || !('data' in env) || 'items' in env)
173
+ return body;
174
+ const meta = asRecord(env.meta);
175
+ if (!meta)
176
+ return body;
177
+ const success = env.success ?? true;
178
+ if (Array.isArray(env.data)) {
179
+ const out = { success, items: env.data };
180
+ for (const [key, value] of Object.entries(meta)) {
181
+ if (!TENNIS_LIST_META_KEYS.has(key))
182
+ out[snakeKey(key)] = value;
183
+ }
184
+ out.total = typeof meta.total === 'number' ? meta.total : env.data.length;
185
+ out.page = typeof meta.page === 'number' ? meta.page : 1;
186
+ out.page_size = typeof meta.limit === 'number' ? meta.limit : env.data.length;
187
+ if (typeof meta.totalPages === 'number')
188
+ out.total_pages = meta.totalPages;
189
+ if (typeof meta.hasNext === 'boolean')
190
+ out.has_next = meta.hasNext;
191
+ if (typeof meta.hasPrev === 'boolean')
192
+ out.has_prev = meta.hasPrev;
193
+ return out;
194
+ }
195
+ const resource = asRecord(env.data);
196
+ if (resource)
197
+ return { success, ...resource };
198
+ return body;
199
+ }
161
200
  export function unwrapPayload(value) {
162
201
  let cur = value;
163
202
  for (let depth = 0; depth < 3; depth += 1) {
@@ -1,77 +1,77 @@
1
- export const SERVER_INSTRUCTIONS = `# Cito MCP — agent operating manual
2
-
3
- You are connected to Cito esports data (read-only). Prefer curated outcome tools over raw REST. Production apps must call Cito REST with the user's API key; use this MCP to design, prototype, and resolve IDs — not as a multi-tenant runtime bus.
4
-
5
- ## Hard rules
6
-
7
- 1. **Never invent IDs.** Do not guess matchId, gameId, playerId, team slug, eventId, or boutId. Always obtain IDs from a prior tool result (resolve, live, schedule, search, or list). If the user gives a name ("T1", "s1mple", "UFC 300"), call resolve_entity/search_entities first.
8
- 2. **Resolve before deep.** Name → ID/slug → summary/page → deep stats. Skip resolve only when the user already provided a Cito ID/slug.
9
- 3. **One screen, one composite.** Prefer a page/summary tool over stitching 4–6 thin GETs.
10
- 4. **Honor the envelope.** Parse ok, data, pagination, partial, error, meta.rateLimit, meta.entities. Partial section failures are not total failures — use successful sections.
11
- 5. **Read-only.** All curated tools are safe to retry except where error.retryable is false for bad args / entitlement.
12
-
13
- ## Game parameter
14
-
15
- game enum (unless a tool documents otherwise): lol | cs2 | dota2 | ufc | cod | tennis | all
16
-
17
- - Default when the user named one title: that game. Default for "what's live?" / multi-title: omit game or all on tools that accept it.
18
- - On UNSUPPORTED_GAME / 403 for a title: stop retrying that game; call api_health; report plan gaps.
19
- - Fortnite and other titles may appear via call_api/resources; do not assume they are in the curated enum until list_capabilities says so.
20
-
21
- ## Preferred tool order
22
-
23
- 1. Unsure → list_capabilities (filter by game or job). Optional: api_health for key/tier/included games.
24
- 2. Name without ID → resolve_entity (best match) or search_entities (browse). Reuse returned id/slug.
25
- 3. Live / upcoming → live_matches; upcoming_schedule.
26
- 4. Match UI / recap → match_summary first. match_details only for timelines, full maps, demos, live state.
27
- 5. Team page → team_profile. Player form → player_profile. Tables/ranks → standings.
28
- 6. Pre-match → match_preview. Rivalry → head_to_head. Event / fight-night card → event_card.
29
- 7. Escape hatch → call_api (allowlisted path prefixes; prefer GET). Prefer curated tools.
30
-
31
- Mnemonic: resolve → live/schedule → summary → deep.
32
-
33
- ## Parallel vs sequence
34
-
35
- Parallel-safe: independent reads (team_profile A ∥ team_profile B; live_matches ∥ api_health). Cap ~3–5 concurrent agent tools; check meta.rateLimit.remaining.
36
-
37
- Serial required: resolve → detail; pagination (cursor from page N only); live board → selected match deep-dive.
38
-
39
- Prefer server-side fan-out inside composites (partial[] recovery) over agent N+1.
40
-
41
- ## Pagination
42
-
43
- Lists use pagination: { limit, offset?, total?, hasMore, nextCursor, prevCursor }. Default limit 20, max 50. Loop with cursor: pagination.nextCursor and the same filters. Never invent cursors. Empty list is ok:true with items:[].
44
-
45
- ## Errors
46
-
47
- ok:false → read error.code (VALIDATION, NOT_FOUND, UNSUPPORTED_GAME, RATE_LIMIT, UNAUTHORIZED, UPSTREAM, PATH_NOT_ALLOWED, NOT_IMPLEMENTED). Follow error.recover[]. Retry only when retryable.
48
-
49
- ok:true with partial[] → use successful sections; do not treat partial as total fail.
50
- ok:true with meta.warnings[] → filters or depth degraded; do not assume ignored filters applied.
51
- resolve_entity ambiguity is soft: ok:true with data.needsDisambiguation and data.candidates — pick a candidate; do not wait for AMBIGUOUS_ENTITY.
52
-
53
- ## Recipes
54
-
55
- Live board: api_health (optional) → live_matches → match_summary for selected matchId.
56
- UFC empty live (count=0): read section.note / emptyReason / health (workerAlive, lag) / supervisor / nextCard — do not claim "API offline" without workerAlive/lag; never invent matchups from supervisor shells.
57
- Team page: resolve_entity {type:team} → team_profile.
58
- Player card: resolve_entity {type:player|fighter} → player_profile.
59
- Fight night / event card: resolve_entity {type:event} → event_card {includeMatches:true} → match_preview for a featured bout.
60
- Match preview (named sides): resolve_entity each side (optional) → match_preview {teamA, teamB}.
61
- App scaffold: list_capabilities ∥ api_health ∥ live_matches, then one composite per screen.
62
-
63
- ## Resources
64
-
65
- - cito://llms.txt — product context when available
66
- - cito://capabilities — catalog summary
67
- - cito://openapi.json — optional public OpenAPI fetch for typed clients
68
-
69
- ## What not to do
70
-
71
- - Invent match/player/team IDs from memory.
72
- - Parallel-paginate the same list with different cursors.
73
- - N+1 match_details for every live row.
74
- - Retry UNSUPPORTED_GAME or VALIDATION unchanged.
75
- - Ship production traffic through MCP.
76
- - Flood odds/timelines when a summary answers the question.
1
+ export const SERVER_INSTRUCTIONS = `# Cito MCP — agent operating manual
2
+
3
+ You are connected to Cito esports data (read-only). Prefer curated outcome tools over raw REST. Production apps must call Cito REST with the user's API key; use this MCP to design, prototype, and resolve IDs — not as a multi-tenant runtime bus.
4
+
5
+ ## Hard rules
6
+
7
+ 1. **Never invent IDs.** Do not guess matchId, gameId, playerId, team slug, eventId, or boutId. Always obtain IDs from a prior tool result (resolve, live, schedule, search, or list). If the user gives a name ("T1", "s1mple", "UFC 300"), call resolve_entity/search_entities first.
8
+ 2. **Resolve before deep.** Name → ID/slug → summary/page → deep stats. Skip resolve only when the user already provided a Cito ID/slug.
9
+ 3. **One screen, one composite.** Prefer a page/summary tool over stitching 4–6 thin GETs.
10
+ 4. **Honor the envelope.** Parse ok, data, pagination, partial, error, meta.rateLimit, meta.entities. Partial section failures are not total failures — use successful sections.
11
+ 5. **Read-only.** All curated tools are safe to retry except where error.retryable is false for bad args / entitlement.
12
+
13
+ ## Game parameter
14
+
15
+ game enum (unless a tool documents otherwise): lol | cs2 | dota2 | ufc | cod | tennis | all
16
+
17
+ - Default when the user named one title: that game. Default for "what's live?" / multi-title: omit game or all on tools that accept it.
18
+ - On UNSUPPORTED_GAME / 403 for a title: stop retrying that game; call api_health; report plan gaps.
19
+ - Fortnite and other titles may appear via call_api/resources; do not assume they are in the curated enum until list_capabilities says so.
20
+
21
+ ## Preferred tool order
22
+
23
+ 1. Unsure → list_capabilities (filter by game or job). Optional: api_health for key/tier/included games.
24
+ 2. Name without ID → resolve_entity (best match) or search_entities (browse). Reuse returned id/slug.
25
+ 3. Live / upcoming → live_matches; upcoming_schedule.
26
+ 4. Match UI / recap → match_summary first. match_details only for timelines, full maps, demos, live state.
27
+ 5. Team page → team_profile. Player form → player_profile. Tables/ranks → standings.
28
+ 6. Pre-match → match_preview. Rivalry → head_to_head. Event / fight-night card → event_card.
29
+ 7. Escape hatch → call_api (allowlisted path prefixes; prefer GET). Prefer curated tools.
30
+
31
+ Mnemonic: resolve → live/schedule → summary → deep.
32
+
33
+ ## Parallel vs sequence
34
+
35
+ Parallel-safe: independent reads (team_profile A ∥ team_profile B; live_matches ∥ api_health). Cap ~3–5 concurrent agent tools; check meta.rateLimit.remaining.
36
+
37
+ Serial required: resolve → detail; pagination (cursor from page N only); live board → selected match deep-dive.
38
+
39
+ Prefer server-side fan-out inside composites (partial[] recovery) over agent N+1.
40
+
41
+ ## Pagination
42
+
43
+ Lists use pagination: { limit, offset?, total?, hasMore, nextCursor, prevCursor }. Default limit 20, max 50. Loop with cursor: pagination.nextCursor and the same filters. Never invent cursors. Empty list is ok:true with items:[].
44
+
45
+ ## Errors
46
+
47
+ ok:false → read error.code (VALIDATION, NOT_FOUND, UNSUPPORTED_GAME, RATE_LIMIT, UNAUTHORIZED, UPSTREAM, PATH_NOT_ALLOWED, NOT_IMPLEMENTED). Follow error.recover[]. Retry only when retryable.
48
+
49
+ ok:true with partial[] → use successful sections; do not treat partial as total fail.
50
+ ok:true with meta.warnings[] → filters or depth degraded; do not assume ignored filters applied.
51
+ resolve_entity ambiguity is soft: ok:true with data.needsDisambiguation and data.candidates — pick a candidate; do not wait for AMBIGUOUS_ENTITY.
52
+
53
+ ## Recipes
54
+
55
+ Live board: api_health (optional) → live_matches → match_summary for selected matchId.
56
+ UFC empty live (count=0): read section.note / emptyReason / health (workerAlive, lag) / supervisor / nextCard — do not claim "API offline" without workerAlive/lag; never invent matchups from supervisor shells.
57
+ Team page: resolve_entity {type:team} → team_profile.
58
+ Player card: resolve_entity {type:player|fighter} → player_profile.
59
+ Fight night / event card: resolve_entity {type:event} → event_card {includeMatches:true} → match_preview for a featured bout.
60
+ Match preview (named sides): resolve_entity each side (optional) → match_preview {teamA, teamB}.
61
+ App scaffold: list_capabilities ∥ api_health ∥ live_matches, then one composite per screen.
62
+
63
+ ## Resources
64
+
65
+ - cito://llms.txt — product context when available
66
+ - cito://capabilities — catalog summary
67
+ - cito://openapi.json — optional public OpenAPI fetch for typed clients
68
+
69
+ ## What not to do
70
+
71
+ - Invent match/player/team IDs from memory.
72
+ - Parallel-paginate the same list with different cursors.
73
+ - N+1 match_details for every live row.
74
+ - Retry UNSUPPORTED_GAME or VALIDATION unchanged.
75
+ - Ship production traffic through MCP.
76
+ - Flood odds/timelines when a summary answers the question.
77
77
  `;
package/dist/scrub.js CHANGED
@@ -15,6 +15,50 @@ const INTERNAL_KEYS = new Set([
15
15
  'wikiUrl',
16
16
  'wiki_url',
17
17
  'jsonLd',
18
+ '_mergeSource',
19
+ '_provenance',
20
+ 'duplicateOf',
21
+ 'duplicate_of',
22
+ 'identityConfidence',
23
+ 'identity_confidence',
24
+ 'roleSource',
25
+ 'role_source',
26
+ 'verifiedCompetitive',
27
+ 'verified_competitive',
28
+ 'profileAttemptedAt',
29
+ 'profileFailureMessage',
30
+ 'profileFailureReason',
31
+ 'profileFetchStatus',
32
+ 'profileImageAttemptedAt',
33
+ 'profileImageAttempts',
34
+ 'profileImageError',
35
+ 'profileImageSteamAttemptedAt',
36
+ 'profileImageWorker',
37
+ 'ratingSource',
38
+ 'requiredSourceData',
39
+ 'rawText',
40
+ 'raw_text',
41
+ 'scrapedAt',
42
+ 'scraped_at',
43
+ 'sourceSummary',
44
+ 'sourceNote',
45
+ 'durableStatsTables',
46
+ 'lineupSource',
47
+ 'mapMediaSource',
48
+ 'rosterSource',
49
+ 'upstream',
50
+ 'liveCandidateWindowMinutes',
51
+ 'rowHash',
52
+ 'row_hash',
53
+ 'snapshotHash',
54
+ 'snapshot_hash',
55
+ 'syncRunId',
56
+ 'sync_run_id',
57
+ 'discoveredSources',
58
+ 'sidecarAvailable',
59
+ 'imageCandidates',
60
+ 'image_candidates',
61
+ 'source_url',
18
62
  ]);
19
63
  const SCRAPE_PARENTS = new Set(['dataAvailability', 'health']);
20
64
  const SCRAPE_KEYS = new Set(['strategy', 'samples']);
@@ -260,20 +260,20 @@ export function composeH2H(game, sideA, sideB, rows, limit) {
260
260
  }
261
261
  export const matchPreview = {
262
262
  name: 'match_preview',
263
- description: `COMPOSITE pre-match briefing: sides, roster/form snippets, H2H stub, event context — for pick'ems, articles, and match-page before state.
264
-
265
- When to use:
266
- - Upcoming match deep link
267
- - "Who should I watch before this game?"
268
- - App scaffold for preview cards
269
-
270
- Prefer over: manually chaining team_profile ×2 + head_to_head + schedule.
271
- Prefer match_summary when match is completed; match_details for live in-game.
272
-
273
- Do not use when: user wants final score/recap of a finished match.
274
-
275
- Parallel-safe: yes. Upstream cost: 4–8.
276
- Tennis H2H accepts an optional surface filter (Hard, Clay, Grass).
263
+ description: `COMPOSITE pre-match briefing: sides, roster/form snippets, H2H stub, event context — for pick'ems, articles, and match-page before state.
264
+
265
+ When to use:
266
+ - Upcoming match deep link
267
+ - "Who should I watch before this game?"
268
+ - App scaffold for preview cards
269
+
270
+ Prefer over: manually chaining team_profile ×2 + head_to_head + schedule.
271
+ Prefer match_summary when match is completed; match_details for live in-game.
272
+
273
+ Do not use when: user wants final score/recap of a finished match.
274
+
275
+ Parallel-safe: yes. Upstream cost: 4–8.
276
+ Tennis H2H accepts an optional surface filter (Hard, Clay, Grass).
277
277
  Example: { "game": "lol", "teamA": "t1", "teamB": "gen-g", "includeH2H": true, "recentLimit": 5 }`,
278
278
  inputSchema: {
279
279
  type: 'object',
@@ -707,31 +707,31 @@ async function resolveEventKey(ctx, game, q) {
707
707
  }
708
708
  export const eventCard = {
709
709
  name: 'event_card',
710
- description: `COMPOSITE event / fight-night card: identity, bout or match list, optional standings snippet.
711
-
712
- When to use:
713
- - UFC fight night / numbered event page ("UFC 300 card", "Fight Night")
714
- - CS2 event hub with match list
715
- - Tournament/event overview before match_preview drill-down
716
-
717
- Bouts come back in card order — main event first, then prelims, then early prelims.
718
- Each bout carries weightClass, titleBout, card placement, and (once fought) result
719
- { method, round, time, referee, winnerSlug }. Each corner carries images
720
- { headshotUrl, bodyImageUrl, imageUrl, proxiedImageUrl }, record, nickname, rank,
721
- championStatus, country and flag when upstream supplies them. Use proxiedImageUrl in
722
- browsers — the image host sends no CORS header. You do not need call_api per fighter for faces.
723
-
724
- Prefer over: agent-side resolve + call_api /ufc/events + bout expansion; N+1 match_summary for the card list only; per-fighter call_api just to fetch headshots.
725
-
726
- Prefer match_preview for one bout/match briefing; match_summary for completed recaps; live_matches for live-only strips; standings alone for pure tables.
727
-
728
- Do not use when: you only need live scores (live_matches); single finished match recap (match_summary); no event name/id yet and game unknown.
729
-
730
- Tennis: pass a tournament id/slug to get the draw bracket — rounds carry a round
731
- code (Q1..R128, QF, SF, F) with each match's players and score. Set is the
732
- primary tennis path here; there is no separate draw tool.
733
-
734
- Parallel-safe: yes. Upstream cost: 1–4.
710
+ description: `COMPOSITE event / fight-night card: identity, bout or match list, optional standings snippet.
711
+
712
+ When to use:
713
+ - UFC fight night / numbered event page ("UFC 300 card", "Fight Night")
714
+ - CS2 event hub with match list
715
+ - Tournament/event overview before match_preview drill-down
716
+
717
+ Bouts come back in card order — main event first, then prelims, then early prelims.
718
+ Each bout carries weightClass, titleBout, card placement, and (once fought) result
719
+ { method, round, time, referee, winnerSlug }. Each corner carries images
720
+ { headshotUrl, bodyImageUrl, imageUrl, proxiedImageUrl }, record, nickname, rank,
721
+ championStatus, country and flag when upstream supplies them. Use proxiedImageUrl in
722
+ browsers — the image host sends no CORS header. You do not need call_api per fighter for faces.
723
+
724
+ Prefer over: agent-side resolve + call_api /ufc/events + bout expansion; N+1 match_summary for the card list only; per-fighter call_api just to fetch headshots.
725
+
726
+ Prefer match_preview for one bout/match briefing; match_summary for completed recaps; live_matches for live-only strips; standings alone for pure tables.
727
+
728
+ Do not use when: you only need live scores (live_matches); single finished match recap (match_summary); no event name/id yet and game unknown.
729
+
730
+ Tennis: pass a tournament id/slug to get the draw bracket — rounds carry a round
731
+ code (Q1..R128, QF, SF, F) with each match's players and score. Set is the
732
+ primary tennis path here; there is no separate draw tool.
733
+
734
+ Parallel-safe: yes. Upstream cost: 1–4.
735
735
  Example: { "game": "ufc", "eventIdOrSlug": "ufc-300", "includeMatches": true, "includeStandings": false }`,
736
736
  inputSchema: {
737
737
  type: 'object',
@@ -154,20 +154,20 @@ function slimUfcNextCard(payload) {
154
154
  }
155
155
  export const liveMatches = {
156
156
  name: 'live_matches',
157
- description: `Live matches board across primary games, or a single game filter. Normalized labels, scores, and matchIds.
158
-
159
- When to use:
160
- - "What's live right now?"
161
- - Ops/dashboard live strip
162
-
163
- Prefer over: sequential per-game call_api live probes.
164
-
165
- Do not use when: user wants upcoming fixtures → upcoming_schedule; historical results → match_summary.
166
-
167
- CS2 live path is /cs2/live; UFC is included in multi-game fan-out.
168
- UFC empty board: section.note + emptyReason + health (workerAlive/lag) + optional supervisor/nextCard (non-live); never fake match items from events[].
169
-
170
- Parallel-safe: yes. Upstream cost: 1–5 (allSettled).
157
+ description: `Live matches board across primary games, or a single game filter. Normalized labels, scores, and matchIds.
158
+
159
+ When to use:
160
+ - "What's live right now?"
161
+ - Ops/dashboard live strip
162
+
163
+ Prefer over: sequential per-game call_api live probes.
164
+
165
+ Do not use when: user wants upcoming fixtures → upcoming_schedule; historical results → match_summary.
166
+
167
+ CS2 live path is /cs2/live; UFC is included in multi-game fan-out.
168
+ UFC empty board: section.note + emptyReason + health (workerAlive/lag) + optional supervisor/nextCard (non-live); never fake match items from events[].
169
+
170
+ Parallel-safe: yes. Upstream cost: 1–5 (allSettled).
171
171
  Example: { "game": "all", "limitPerGame": 10 }`,
172
172
  inputSchema: {
173
173
  type: 'object',
@@ -347,24 +347,24 @@ Example: { "game": "all", "limitPerGame": 10 }`,
347
347
  };
348
348
  export const upcomingSchedule = {
349
349
  name: 'upcoming_schedule',
350
- description: `Upcoming matches/events for one game, with game-specific filters.
351
-
352
- When to use:
353
- - "What's on this week?"
354
- - Calendar UI; team next matches
355
-
356
- Prefer over: live_matches for not-yet-started fixtures.
357
-
358
- Do not use when: only in-progress matches needed → live_matches.
359
-
360
- Filter support (unsupported params are ignored with meta.warnings — do not assume filtering worked):
361
- - lol: hours, team (slug), league (slug)
362
- - cs2: team, from, to (ISO); hours not applied upstream
363
- - cod: team, tournamentId
364
- - dota2: limit/cursor primarily; team may be client-filtered where data allows
365
- - ufc: hours / from / to applied client-side after bout expansion (API has no hours); event shells labeled by event name; bouts use fighters[] corners
366
-
367
- Parallel-safe: yes. Upstream cost: 1–2.
350
+ description: `Upcoming matches/events for one game, with game-specific filters.
351
+
352
+ When to use:
353
+ - "What's on this week?"
354
+ - Calendar UI; team next matches
355
+
356
+ Prefer over: live_matches for not-yet-started fixtures.
357
+
358
+ Do not use when: only in-progress matches needed → live_matches.
359
+
360
+ Filter support (unsupported params are ignored with meta.warnings — do not assume filtering worked):
361
+ - lol: hours, team (slug), league (slug)
362
+ - cs2: team, from, to (ISO); hours not applied upstream
363
+ - cod: team, tournamentId
364
+ - dota2: limit/cursor primarily; team may be client-filtered where data allows
365
+ - ufc: hours / from / to applied client-side after bout expansion (API has no hours); event shells labeled by event name; bouts use fighters[] corners
366
+
367
+ Parallel-safe: yes. Upstream cost: 1–2.
368
368
  Example: { "game": "lol", "hours": 72, "team": "t1", "limit": 20 }`,
369
369
  inputSchema: {
370
370
  type: 'object',
@@ -163,18 +163,18 @@ function primaryPath(game, matchId) {
163
163
  }
164
164
  export const matchSummary = {
165
165
  name: 'match_summary',
166
- description: `COMPOSITE match card: scoreline, key context, player performances, and VOD/demo links when available.
167
-
168
- When to use:
169
- - Match recap / default match UI
170
- - After user selects a live or completed matchId
171
-
172
- Prefer over match_details for chat answers and default UIs.
173
- Prefer match_details for timelines, full map trees, live state, advanced packages.
174
-
175
- Do not use when: no matchId yet (resolve from live/schedule); pure pre-match → match_preview.
176
-
177
- Parallel-safe: yes. Upstream cost: 2–5.
166
+ description: `COMPOSITE match card: scoreline, key context, player performances, and VOD/demo links when available.
167
+
168
+ When to use:
169
+ - Match recap / default match UI
170
+ - After user selects a live or completed matchId
171
+
172
+ Prefer over match_details for chat answers and default UIs.
173
+ Prefer match_details for timelines, full map trees, live state, advanced packages.
174
+
175
+ Do not use when: no matchId yet (resolve from live/schedule); pure pre-match → match_preview.
176
+
177
+ Parallel-safe: yes. Upstream cost: 2–5.
178
178
  Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includePlayerStats": true }`,
179
179
  inputSchema: {
180
180
  type: 'object',
@@ -586,23 +586,23 @@ export function summarizeTennisMatchStats(data) {
586
586
  }
587
587
  export const matchDetails = {
588
588
  name: 'match_details',
589
- description: `Deep match package: optional timelines, advanced stats, live state/snapshots, full map/game tree, media inventory.
590
-
591
- When to use:
592
- - Analyst deep dive
593
- - Live in-game window (LoL/CS2/UFC)
594
- - Full demo list
595
-
596
- Prefer over match_summary only when summary is insufficient.
597
- Prefer match_summary for short answers and default cards.
598
-
599
- Do not use when: first-pass live board (use live_matches + match_summary).
600
-
601
- Section selection: pass includeTimeline / includeLiveState / includeAdvanced booleans, OR an explicit sections[] list.
602
- UFC betting lines: sections:["odds"] (opt-in, never in the default set).
603
- If sections[] is non-empty it wins (booleans are ignored). LoL liveState/advanced require gameId.
604
-
605
- Parallel-safe: yes. Upstream cost: 1–8 (section-gated).
589
+ description: `Deep match package: optional timelines, advanced stats, live state/snapshots, full map/game tree, media inventory.
590
+
591
+ When to use:
592
+ - Analyst deep dive
593
+ - Live in-game window (LoL/CS2/UFC)
594
+ - Full demo list
595
+
596
+ Prefer over match_summary only when summary is insufficient.
597
+ Prefer match_summary for short answers and default cards.
598
+
599
+ Do not use when: first-pass live board (use live_matches + match_summary).
600
+
601
+ Section selection: pass includeTimeline / includeLiveState / includeAdvanced booleans, OR an explicit sections[] list.
602
+ UFC betting lines: sections:["odds"] (opt-in, never in the default set).
603
+ If sections[] is non-empty it wins (booleans are ignored). LoL liveState/advanced require gameId.
604
+
605
+ Parallel-safe: yes. Upstream cost: 1–8 (section-gated).
606
606
  Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "includeLiveState": false }`,
607
607
  inputSchema: {
608
608
  type: 'object',
@@ -834,7 +834,7 @@ Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"pag
834
834
  });
835
835
  }
836
836
  }
837
- const res = await fetchJson(ctx, path, { method, query });
837
+ const res = await fetchJson(ctx, path, { method, query, raw: true });
838
838
  if (!res.ok) {
839
839
  return errorEnvelope({
840
840
  code: mapHttpToCode(res.status, { gameNotIncluded: gameNotIncludedHint(res.data) }),
@@ -44,17 +44,17 @@ function identityFrom(game, raw, idHint, slugHint) {
44
44
  }
45
45
  export const playerProfile = {
46
46
  name: 'player_profile',
47
- description: `Player or UFC fighter profile: identity, current team, recent matches, and form/trends/radar when available.
48
-
49
- When to use:
50
- - "How is X playing lately?"
51
- - Player page scaffold; form inputs for previews
52
-
53
- Prefer over: manual multi-call career/trends/matches via call_api.
54
-
55
- Do not use when: full team roster needed → team_profile; unresolved name → resolve_entity first.
56
-
57
- Parallel-safe: yes. Upstream cost: 2–5.
47
+ description: `Player or UFC fighter profile: identity, current team, recent matches, and form/trends/radar when available.
48
+
49
+ When to use:
50
+ - "How is X playing lately?"
51
+ - Player page scaffold; form inputs for previews
52
+
53
+ Prefer over: manual multi-call career/trends/matches via call_api.
54
+
55
+ Do not use when: full team roster needed → team_profile; unresolved name → resolve_entity first.
56
+
57
+ Parallel-safe: yes. Upstream cost: 2–5.
58
58
  Example: { "game": "cs2", "playerId": "cs2-player-1", "recentLimit": 10, "includeTrends": true }`,
59
59
  inputSchema: {
60
60
  type: 'object',
@@ -541,17 +541,17 @@ function normalizeFormMatch(row) {
541
541
  }
542
542
  export const playerForm = {
543
543
  name: 'player_form',
544
- description: `Tennis player form: W/L record over the last N completed matches, current win/loss streak, and per-match rows (opponent, score, surface). Optional surface filter (Hard/Clay/Grass).
545
-
546
- When to use:
547
- - "How is X playing lately?"; current streak; surface-specific record (e.g. clay last 5).
548
-
549
- Prefer over: player_profile (identity + career aggregates, no streak); raw form via call_api for agent-normalized rows.
550
-
551
- Do not use when: career totals/titles → player_profile with game tennis (wires /stats); ranking deltas → rankings_movers.
552
-
553
- Tennis-only. Limit defaults to 10 (max 50, matching the API). Upstream rows arrive newest-first; the streak is the leading run of that order.
554
-
544
+ description: `Tennis player form: W/L record over the last N completed matches, current win/loss streak, and per-match rows (opponent, score, surface). Optional surface filter (Hard/Clay/Grass).
545
+
546
+ When to use:
547
+ - "How is X playing lately?"; current streak; surface-specific record (e.g. clay last 5).
548
+
549
+ Prefer over: player_profile (identity + career aggregates, no streak); raw form via call_api for agent-normalized rows.
550
+
551
+ Do not use when: career totals/titles → player_profile with game tennis (wires /stats); ranking deltas → rankings_movers.
552
+
553
+ Tennis-only. Limit defaults to 10 (max 50, matching the API). Upstream rows arrive newest-first; the streak is the leading run of that order.
554
+
555
555
  Parallel-safe: yes. Upstream cost: 1.`,
556
556
  inputSchema: {
557
557
  type: 'object',
@@ -700,18 +700,18 @@ Parallel-safe: yes. Upstream cost: 1.`,
700
700
  };
701
701
  export const playerMatches = {
702
702
  name: 'player_matches',
703
- description: `Tennis player match log: every archived match for one player, newest first, with opponent, tournament, round, surface and score.
704
-
705
- When to use:
706
- - "Show me Shelton's last 20 matches"; full match history; every match at a given tournament; filtering a player's record by surface.
707
-
708
- Prefer over: player_form (a W/L summary with a small window and a streak, not the log); call_api for /tennis/players/{id}/matches.
709
-
710
- Do not use when: aggregate totals, titles, or win% → player_stats; identity → player_profile; one match's box score → match_details.
711
-
712
- Tennis-only. Limit defaults to 20 (max 50). Rows arrive newest-first. Use
713
- surface to narrow to Hard/Clay/Grass; tournamentId pins one event.
714
-
703
+ description: `Tennis player match log: every archived match for one player, newest first, with opponent, tournament, round, surface and score.
704
+
705
+ When to use:
706
+ - "Show me Shelton's last 20 matches"; full match history; every match at a given tournament; filtering a player's record by surface.
707
+
708
+ Prefer over: player_form (a W/L summary with a small window and a streak, not the log); call_api for /tennis/players/{id}/matches.
709
+
710
+ Do not use when: aggregate totals, titles, or win% → player_stats; identity → player_profile; one match's box score → match_details.
711
+
712
+ Tennis-only. Limit defaults to 20 (max 50). Rows arrive newest-first. Use
713
+ surface to narrow to Hard/Clay/Grass; tournamentId pins one event.
714
+
715
715
  Parallel-safe: yes. Upstream cost: 1.`,
716
716
  inputSchema: {
717
717
  type: 'object',
@@ -869,16 +869,16 @@ Parallel-safe: yes. Upstream cost: 1.`,
869
869
  };
870
870
  export const playerStats = {
871
871
  name: 'player_stats',
872
- description: `Tennis player career statistics: W/L totals, win percentage, titles, Grand Slam and Masters titles, plus per-surface and per-level breakdowns. Optional surface/year filters.
873
-
874
- When to use:
875
- - "How many titles does Alcaraz have?"; career win%; Grand Slam title count; record on clay; a season-scoped record.
876
-
877
- Prefer over: player_profile (identity + a summary block, no breakdowns); player_matches (individual rows, no aggregates).
878
-
879
- Do not use when: the match log → player_matches; ranking over time → player_rankings_history.
880
-
881
- Tennis-only. Filters are surface (Hard/Clay/Grass/Carpet) and yearFrom/yearTo.
872
+ description: `Tennis player career statistics: W/L totals, win percentage, titles, Grand Slam and Masters titles, plus per-surface and per-level breakdowns. Optional surface/year filters.
873
+
874
+ When to use:
875
+ - "How many titles does Alcaraz have?"; career win%; Grand Slam title count; record on clay; a season-scoped record.
876
+
877
+ Prefer over: player_profile (identity + a summary block, no breakdowns); player_matches (individual rows, no aggregates).
878
+
879
+ Do not use when: the match log → player_matches; ranking over time → player_rankings_history.
880
+
881
+ Tennis-only. Filters are surface (Hard/Clay/Grass/Carpet) and yearFrom/yearTo.
882
882
  Upstream cost: 1.`,
883
883
  inputSchema: {
884
884
  type: 'object',
@@ -77,26 +77,26 @@ export function normalizeStandingRow(row, index) {
77
77
  }
78
78
  export const standings = {
79
79
  name: 'standings',
80
- description: `League/event standings or world/division rankings normalized to ranked rows.
81
-
82
- When to use:
83
- - Table / playoff picture
84
- - UFC rankings; CS2 world or event standings; CDL standings; LoL league/tournament tables
85
-
86
- Prefer over: raw standings via call_api for agent-normalized rows.
87
-
88
- Do not use when: single team form → team_profile; live scores → live_matches.
89
- Dota has no first-class standings (may NOT_IMPLEMENTED or weak worldRanking).
90
-
91
- Required scope keys by game:
92
- - lol: leagueId OR tournamentId
93
- - cs2: omit for world rankings; eventId for event standings
94
- - cod: optional season/stage
95
- - ufc: optional division (scope=division)
96
- - tennis: optional division (ATP or WTA tour; default ATP)
97
- - dota2: best-effort worldRanking only
98
-
99
- Parallel-safe: yes. Upstream cost: 1–2.
80
+ description: `League/event standings or world/division rankings normalized to ranked rows.
81
+
82
+ When to use:
83
+ - Table / playoff picture
84
+ - UFC rankings; CS2 world or event standings; CDL standings; LoL league/tournament tables
85
+
86
+ Prefer over: raw standings via call_api for agent-normalized rows.
87
+
88
+ Do not use when: single team form → team_profile; live scores → live_matches.
89
+ Dota has no first-class standings (may NOT_IMPLEMENTED or weak worldRanking).
90
+
91
+ Required scope keys by game:
92
+ - lol: leagueId OR tournamentId
93
+ - cs2: omit for world rankings; eventId for event standings
94
+ - cod: optional season/stage
95
+ - ufc: optional division (scope=division)
96
+ - tennis: optional division (ATP or WTA tour; default ATP)
97
+ - dota2: best-effort worldRanking only
98
+
99
+ Parallel-safe: yes. Upstream cost: 1–2.
100
100
  Example: { "game": "cod", "season": "2026", "limit": 50 }`,
101
101
  inputSchema: {
102
102
  type: 'object',
@@ -31,18 +31,18 @@ function numberOrNull(value) {
31
31
  }
32
32
  export const teamProfile = {
33
33
  name: 'team_profile',
34
- description: `Team/org card: identity, roster, recent matches, and form/trends/radar when available.
35
-
36
- When to use:
37
- - Team page / "who is on this roster?"
38
- - Builder team screen sample
39
-
40
- Prefer over: separate roster + matches + detail via call_api.
41
-
42
- Do not use when: UFC fighters → player_profile; unknown name → resolve_entity first.
43
- Dota may return partial roster (API gap). Prefer slug for lol/cod; teamId for cs2.
44
-
45
- Parallel-safe: yes. Upstream cost: 2–4.
34
+ description: `Team/org card: identity, roster, recent matches, and form/trends/radar when available.
35
+
36
+ When to use:
37
+ - Team page / "who is on this roster?"
38
+ - Builder team screen sample
39
+
40
+ Prefer over: separate roster + matches + detail via call_api.
41
+
42
+ Do not use when: UFC fighters → player_profile; unknown name → resolve_entity first.
43
+ Dota may return partial roster (API gap). Prefer slug for lol/cod; teamId for cs2.
44
+
45
+ Parallel-safe: yes. Upstream cost: 2–4.
46
46
  Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
47
47
  inputSchema: {
48
48
  type: 'object',
@@ -532,19 +532,19 @@ function winnerSide(match, a) {
532
532
  }
533
533
  export const headToHead = {
534
534
  name: 'head_to_head',
535
- description: `Composed head-to-head record between two teams, two UFC fighters, or two tennis players. No first-class REST H2H exists — this tool filters match history server-side.
536
-
537
- When to use:
538
- - Rivalry / series record questions
539
- - Supporting context for previews
540
-
541
- Prefer over: agent-side double match-list filtering.
542
-
543
- Do not use when: single-side form only → team_profile or player_profile.
544
-
545
- Caveat: Dota filters are weaker; expect meta.warnings when data is sparse.
546
-
547
- Parallel-safe: yes. Upstream cost: 2–4.
535
+ description: `Composed head-to-head record between two teams, two UFC fighters, or two tennis players. No first-class REST H2H exists — this tool filters match history server-side.
536
+
537
+ When to use:
538
+ - Rivalry / series record questions
539
+ - Supporting context for previews
540
+
541
+ Prefer over: agent-side double match-list filtering.
542
+
543
+ Do not use when: single-side form only → team_profile or player_profile.
544
+
545
+ Caveat: Dota filters are weaker; expect meta.warnings when data is sparse.
546
+
547
+ Parallel-safe: yes. Upstream cost: 2–4.
548
548
  Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
549
549
  inputSchema: {
550
550
  type: 'object',
@@ -960,18 +960,18 @@ Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
960
960
  };
961
961
  export const h2hMatrix = {
962
962
  name: 'h2h_matrix',
963
- description: `Multi-player tennis head-to-head grid: every pair's series record in one comparison matrix.
964
-
965
- When to use:
966
- - Draw/field analysis: how each contender fares against every other (e.g. Alcaraz vs Zverev vs Sinner round-robin records)
967
- - Group-stage or semifinal-field comparisons
968
-
969
- Prefer over: N head_to_head calls for an N-player field; raw matrix via call_api.
970
-
971
- Do not use when: a two-player rivalry deep-dive with tiebreak/decider splits → head_to_head; season stat leaders → leaderboard_*; rankings → standings.
972
-
973
- Tennis-only. players takes 2-16 ids or names (names resolve via player search). Each matrix cell is "W-L" from the row player's perspective, "-" on the diagonal.
974
-
963
+ description: `Multi-player tennis head-to-head grid: every pair's series record in one comparison matrix.
964
+
965
+ When to use:
966
+ - Draw/field analysis: how each contender fares against every other (e.g. Alcaraz vs Zverev vs Sinner round-robin records)
967
+ - Group-stage or semifinal-field comparisons
968
+
969
+ Prefer over: N head_to_head calls for an N-player field; raw matrix via call_api.
970
+
971
+ Do not use when: a two-player rivalry deep-dive with tiebreak/decider splits → head_to_head; season stat leaders → leaderboard_*; rankings → standings.
972
+
973
+ Tennis-only. players takes 2-16 ids or names (names resolve via player search). Each matrix cell is "W-L" from the row player's perspective, "-" on the diagonal.
974
+
975
975
  Parallel-safe: yes. Upstream cost: 1 + one search per unresolved name.`,
976
976
  inputSchema: {
977
977
  type: 'object',
package/package.json CHANGED
@@ -1,8 +1,28 @@
1
1
  {
2
2
  "name": "cito-mcp",
3
- "version": "0.4.5",
4
- "description": "Standalone MCP server for the Cito esports and sports API — 42 curated outcome tools for agents (live scoreboards, round economy, opening duels, clutches, vetoes, rosters, tennis, mma).",
3
+ "version": "0.4.6",
4
+ "description": "Standalone MCP server for the Cito esports and sports API — curated outcome tools for agents (live scoreboards, round economy, opening duels, clutches, vetoes, rosters, tennis, mma).",
5
5
  "type": "module",
6
+ "homepage": "https://cito.gg",
7
+ "//REPOSITORY": "Intentionally NOT set. cito-api-scraper and cito.gg are PRIVATE repos and must stay that way, so a repository URL pointing at them would render as a broken 404 link on the public npm page and would not satisfy MCP Registry npm ownership validation. Set this only if a PUBLIC repo for this package is ever created (e.g. achillesscriptsvip/cito-mcp with this directory as root) — then use `git+https://github.com/OWNER/REPO.git` and mirror it in the `mcpName`/server.json `name` pair. Do not point it at the private monorepo.",
8
+ "keywords": [
9
+ "mcp",
10
+ "model-context-protocol",
11
+ "esports",
12
+ "cs2",
13
+ "counter-strike",
14
+ "counter-strike-2",
15
+ "lol",
16
+ "league-of-legends",
17
+ "dota2",
18
+ "cod",
19
+ "ufc",
20
+ "mma",
21
+ "tennis",
22
+ "live-scores",
23
+ "sports-data",
24
+ "esports-api"
25
+ ],
6
26
  "bin": {
7
27
  "cito-mcp": "dist/index.js"
8
28
  },