cito-mcp 0.5.8 → 0.6.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 +21 -21
- package/dist/envelope.js +9 -9
- package/dist/index.js +2 -2
- package/dist/instructions.js +10 -4
- package/dist/scrub.js +42 -4
- package/dist/tools/combat.js +6 -6
- package/dist/tools/index.js +2 -0
- package/dist/tools/insight.js +12 -10
- package/dist/tools/leaderboard.js +2 -2
- package/dist/tools/live.js +8 -7
- package/dist/tools/match.js +65 -22
- package/dist/tools/meta.js +46 -11
- package/dist/tools/normalize.js +3 -2
- package/dist/tools/player.js +44 -5
- package/dist/tools/rankings.js +35 -2
- package/dist/tools/resolve.js +11 -11
- package/dist/tools/schedule.js +7 -6
- package/dist/tools/standings.js +43 -23
- package/dist/tools/team.js +8 -6
- package/dist/tools/tournaments.js +5 -7
- package/dist/tools/types.js +1 -1
- package/dist/tools/webhooks.js +377 -0
- package/package.json +1 -1
package/dist/tools/match.js
CHANGED
|
@@ -182,7 +182,7 @@ Prefer match_details for timelines, full map trees, live state, advanced package
|
|
|
182
182
|
|
|
183
183
|
Do not use when: no matchId yet (resolve from live/schedule); pure pre-match → match_preview.
|
|
184
184
|
|
|
185
|
-
Parallel-safe: yes.
|
|
185
|
+
Parallel-safe: yes. API calls: 2–5.
|
|
186
186
|
Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includePlayerStats": true }`,
|
|
187
187
|
inputSchema: {
|
|
188
188
|
type: 'object',
|
|
@@ -344,6 +344,7 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
344
344
|
player1Games: p1g,
|
|
345
345
|
player2Games: p2g,
|
|
346
346
|
tiebreak: sr.tiebreak ?? null,
|
|
347
|
+
tiebreakPoints: sr.tiebreak_points ?? null,
|
|
347
348
|
completed: tennisSetCompleted(sr),
|
|
348
349
|
};
|
|
349
350
|
});
|
|
@@ -404,6 +405,37 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
404
405
|
});
|
|
405
406
|
},
|
|
406
407
|
};
|
|
408
|
+
export function summarizeCs2Odds(data) {
|
|
409
|
+
const root = (data ?? {});
|
|
410
|
+
const payload = (root.data && typeof root.data === 'object' && !Array.isArray(root.data)
|
|
411
|
+
? root.data
|
|
412
|
+
: root);
|
|
413
|
+
const rows = Array.isArray(payload.odds) ? payload.odds : [];
|
|
414
|
+
const rank = (m) => (m === 'h2h' ? '0' : `1${String(m ?? '')}`);
|
|
415
|
+
const markets = rows
|
|
416
|
+
.map((r) => ({
|
|
417
|
+
market: r.market ?? null,
|
|
418
|
+
team1Odds: r.team1Odds ?? null,
|
|
419
|
+
team2Odds: r.team2Odds ?? null,
|
|
420
|
+
team1ImpliedProb: r.team1ImpliedProb ?? null,
|
|
421
|
+
team2ImpliedProb: r.team2ImpliedProb ?? null,
|
|
422
|
+
marketStatus: r.marketStatus ?? 'open',
|
|
423
|
+
liquidityUsd: r.liquidityUsd ?? null,
|
|
424
|
+
capturedAt: r.capturedAt ?? null,
|
|
425
|
+
}))
|
|
426
|
+
.sort((a, b) => rank(a.market).localeCompare(rank(b.market)));
|
|
427
|
+
return {
|
|
428
|
+
teams: payload.teams ?? null,
|
|
429
|
+
dataAvailable: markets.length > 0,
|
|
430
|
+
markets,
|
|
431
|
+
realTime: {
|
|
432
|
+
updates: 'Prices update as soon as the market price changes, through every map and between rounds.',
|
|
433
|
+
push: 'wss://api.citoapi.com/api/v1/cs2/live/ws (message type cs2.odds.updated; needs Live WebSockets and a plan with odds)',
|
|
434
|
+
byRound: '/cs2/matches/{matchId}/odds/rounds (price at each round start and end); /cs2/matches/{matchId}/odds/history?map=&round=&phase=',
|
|
435
|
+
},
|
|
436
|
+
fullBookPath: '/cs2/matches/{matchId}/odds',
|
|
437
|
+
};
|
|
438
|
+
}
|
|
407
439
|
export function summarizeUfcOdds(data) {
|
|
408
440
|
const root = (data ?? {});
|
|
409
441
|
const payload = (root.data && typeof root.data === 'object' && !Array.isArray(root.data)
|
|
@@ -482,8 +514,7 @@ export function summarizeTennisSetStats(block) {
|
|
|
482
514
|
return null;
|
|
483
515
|
const arr = (v) => (Array.isArray(v) ? v : []);
|
|
484
516
|
const strings = (v) => arr(v).map(String);
|
|
485
|
-
if (!arr(b.periods).length && !arr(b.gaps).length &&
|
|
486
|
-
&& typeof b.note !== 'string') {
|
|
517
|
+
if (!arr(b.periods).length && !arr(b.gaps).length && typeof b.note !== 'string') {
|
|
487
518
|
return null;
|
|
488
519
|
}
|
|
489
520
|
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
@@ -514,37 +545,45 @@ export function summarizeTennisSetStats(block) {
|
|
|
514
545
|
period: str(pr.period) ?? 'ALL',
|
|
515
546
|
setNumber: num(pr.set_number) ?? 0,
|
|
516
547
|
status: str(pr.status) ?? 'partial',
|
|
517
|
-
sources: strings(pr.sources),
|
|
518
548
|
winner: line(pr.winner),
|
|
519
549
|
loser: line(pr.loser),
|
|
520
550
|
missing: { winner: strings(missing.winner), loser: strings(missing.loser) },
|
|
521
551
|
};
|
|
522
552
|
}),
|
|
523
|
-
sources: arr(b.sources).map((s) => {
|
|
524
|
-
const sr = asRecord(s) ?? {};
|
|
525
|
-
return {
|
|
526
|
-
source: str(sr.source) ?? 'unknown',
|
|
527
|
-
status: str(sr.status) ?? 'unknown',
|
|
528
|
-
fields: strings(sr.fields),
|
|
529
|
-
detail: str(sr.detail),
|
|
530
|
-
};
|
|
531
|
-
}),
|
|
532
553
|
gaps: arr(b.gaps).map((g) => {
|
|
533
554
|
const gr = asRecord(g) ?? {};
|
|
555
|
+
const rawCode = str(gr.code) ?? 'UNKNOWN';
|
|
556
|
+
const originGap = /SOURCE/i.test(rawCode);
|
|
557
|
+
const fields = strings(gr.fields);
|
|
558
|
+
const rawMessage = str(gr.message) ?? '';
|
|
534
559
|
return {
|
|
535
|
-
code:
|
|
560
|
+
code: originGap ? 'STATS_UNAVAILABLE' : rawCode,
|
|
536
561
|
period: str(gr.period),
|
|
537
562
|
setNumber: num(gr.set_number),
|
|
538
563
|
side: str(gr.side),
|
|
539
|
-
fields
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
564
|
+
fields,
|
|
565
|
+
status: originGap ? null : str(gr.status),
|
|
566
|
+
message: originGap || GAP_INTERNAL_WORDING.test(rawMessage)
|
|
567
|
+
? fields.length
|
|
568
|
+
? `${fields.join(', ')} not available`
|
|
569
|
+
: 'Some statistics are not available for this match'
|
|
570
|
+
: rawMessage,
|
|
543
571
|
};
|
|
544
572
|
}),
|
|
545
|
-
note: str(b.note),
|
|
573
|
+
note: neutralSetStatsNote(str(b.note)),
|
|
546
574
|
};
|
|
547
575
|
}
|
|
576
|
+
const GAP_INTERNAL_WORDING = /\bsources?\b|source_|ingest|feed|vendor|\b[a-z_]+\.[a-z_]+\b/i;
|
|
577
|
+
function neutralSetStatsNote(note) {
|
|
578
|
+
if (!note)
|
|
579
|
+
return note;
|
|
580
|
+
const kept = note
|
|
581
|
+
.split(/(?<=\.)\s+/)
|
|
582
|
+
.filter((sentence) => !/\bsource/i.test(sentence))
|
|
583
|
+
.join(' ')
|
|
584
|
+
.trim();
|
|
585
|
+
return kept || null;
|
|
586
|
+
}
|
|
548
587
|
export function tennisSetStatsWarnings(block) {
|
|
549
588
|
const shaped = summarizeTennisSetStats(block);
|
|
550
589
|
if (!shaped)
|
|
@@ -602,6 +641,7 @@ export function summarizeTennisMatchStats(data) {
|
|
|
602
641
|
player1Games: toNum(sr.player1_games),
|
|
603
642
|
player2Games: toNum(sr.player2_games),
|
|
604
643
|
tiebreak: sr.tiebreak ?? null,
|
|
644
|
+
tiebreakPoints: sr.tiebreak_points ?? null,
|
|
605
645
|
completed: tennisSetCompleted(sr),
|
|
606
646
|
};
|
|
607
647
|
}),
|
|
@@ -624,10 +664,10 @@ Prefer match_summary for short answers and default cards.
|
|
|
624
664
|
Do not use when: first-pass live board (use live_matches + match_summary).
|
|
625
665
|
|
|
626
666
|
Section selection: pass includeTimeline / includeLiveState / includeAdvanced booleans, OR an explicit sections[] list.
|
|
627
|
-
|
|
667
|
+
Betting lines: sections:["odds"] (opt-in, never in the default set): UFC moneyline summary, or CS2 series + map markets (real-time prices, pushed over the CS2 live WebSocket as cs2.odds.updated).
|
|
628
668
|
If sections[] is non-empty it wins (booleans are ignored). LoL liveState/advanced require gameId.
|
|
629
669
|
|
|
630
|
-
Parallel-safe: yes.
|
|
670
|
+
Parallel-safe: yes. API calls: 1–8 (section-gated).
|
|
631
671
|
Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "includeLiveState": false }`,
|
|
632
672
|
inputSchema: {
|
|
633
673
|
type: 'object',
|
|
@@ -831,10 +871,13 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
831
871
|
if (game === 'ufc') {
|
|
832
872
|
loadWith('odds', `/ufc/bouts/${encodeURIComponent(matchId)}/odds`, summarizeUfcOdds);
|
|
833
873
|
}
|
|
874
|
+
else if (game === 'cs2') {
|
|
875
|
+
loadWith('odds', `/cs2/matches/${encodeURIComponent(matchId)}/odds`, summarizeCs2Odds);
|
|
876
|
+
}
|
|
834
877
|
else {
|
|
835
878
|
partial.push(partialFromRejection('odds', {
|
|
836
879
|
code: 'NOT_IMPLEMENTED',
|
|
837
|
-
message: `Odds are
|
|
880
|
+
message: `Odds sections are available for UFC (/ufc/bouts/{id}/odds) and CS2 (/cs2/matches/{id}/odds); LoL odds via call_api /lol/matches/{id}/odds.`,
|
|
838
881
|
}));
|
|
839
882
|
}
|
|
840
883
|
}
|
package/dist/tools/meta.js
CHANGED
|
@@ -65,6 +65,36 @@ const COMBAT_CATALOG = [
|
|
|
65
65
|
exampleArgs: { league: 'bkfc', division: 'heavyweight' },
|
|
66
66
|
},
|
|
67
67
|
];
|
|
68
|
+
const WEBHOOK_GAMES = [...PRIMARY_GAMES];
|
|
69
|
+
const webhookCatalogRow = (name, outcome, exampleArgs, parallelSafe = false) => ({
|
|
70
|
+
name,
|
|
71
|
+
outcome,
|
|
72
|
+
parallelSafe,
|
|
73
|
+
games: WEBHOOK_GAMES,
|
|
74
|
+
jobs: ['webhooks'],
|
|
75
|
+
exampleArgs,
|
|
76
|
+
preferOver: [],
|
|
77
|
+
doNotUse: 'Reading match data → the curated data tools',
|
|
78
|
+
});
|
|
79
|
+
const WEBHOOK_CATALOG = [
|
|
80
|
+
webhookCatalogRow('explain_webhook_setup', 'How webhooks work plus this account: plan, existing webhooks and inboxes, next steps', {}, true),
|
|
81
|
+
webhookCatalogRow('list_webhook_events', 'Every webhook event with status, measured frequency, plan, filters; fields and a real example for one event', { game: 'cs2' }, true),
|
|
82
|
+
webhookCatalogRow('list_webhooks', "This account's webhooks and their health", {}, true),
|
|
83
|
+
webhookCatalogRow('create_webhook', 'Create a webhook on this account (Pro or Scale); the secret comes back once', { url: 'https://your-domain.com/webhooks/cito', events: ['cs2.match.completed'] }),
|
|
84
|
+
webhookCatalogRow('update_webhook', "Change a webhook's URL, events, filters or on/off state", { id: 'from list_webhooks', isActive: false }),
|
|
85
|
+
webhookCatalogRow('delete_webhook', 'Delete a webhook; needs confirm: true and the user clearly asking', { id: 'from list_webhooks', confirm: true }),
|
|
86
|
+
webhookCatalogRow('regenerate_webhook_secret', "Replace a webhook's signing secret", { id: 'from list_webhooks' }),
|
|
87
|
+
webhookCatalogRow('test_webhook', 'Send a real signed sample and get status, code, latency and a diagnosis', { id: 'from list_webhooks', event: 'cs2.match.completed' }),
|
|
88
|
+
webhookCatalogRow('webhook_deliveries', "A webhook's delivery log with diagnosis; one delivery with its payload", { id: 'from list_webhooks', status: 'failed' }, true),
|
|
89
|
+
webhookCatalogRow('replay_webhook', 'Re-send a stored event or a match to this webhook', { id: 'from list_webhooks', eventId: 'from webhook_deliveries' }),
|
|
90
|
+
webhookCatalogRow('catch_up_webhook', 'After an outage, re-send the match and result events a webhook missed', { id: 'from list_webhooks', dryRun: true }),
|
|
91
|
+
webhookCatalogRow('create_webhook_inbox', 'A hosted temporary receiver, on any plan, so no server or tunnel is needed', {}),
|
|
92
|
+
webhookCatalogRow('list_webhook_inboxes', "This account's live test inboxes and limits", {}, true),
|
|
93
|
+
webhookCatalogRow('read_webhook_inbox', 'What an inbox received: headers, body, signature check', { id: 'from create_webhook_inbox' }, true),
|
|
94
|
+
webhookCatalogRow('send_inbox_sample', 'Put a real sample of an event into an inbox, on any plan', { id: 'from create_webhook_inbox', event: 'cs2.match.completed' }),
|
|
95
|
+
webhookCatalogRow('clear_webhook_inbox', 'Remove what an inbox holds', { id: 'from create_webhook_inbox' }),
|
|
96
|
+
webhookCatalogRow('delete_webhook_inbox', 'Delete a test inbox', { id: 'from create_webhook_inbox' }),
|
|
97
|
+
];
|
|
68
98
|
const TOOL_CATALOG = [
|
|
69
99
|
{
|
|
70
100
|
name: 'list_capabilities',
|
|
@@ -168,7 +198,7 @@ const TOOL_CATALOG = [
|
|
|
168
198
|
},
|
|
169
199
|
{
|
|
170
200
|
name: 'head_to_head',
|
|
171
|
-
outcome: '
|
|
201
|
+
outcome: 'H2H record between two teams, fighters or tennis players (sideA, sideB; h2h_matrix takes players)',
|
|
172
202
|
parallelSafe: true,
|
|
173
203
|
games: [...PRIMARY_GAMES],
|
|
174
204
|
jobs: ['h2h', 'preview'],
|
|
@@ -228,7 +258,7 @@ const TOOL_CATALOG = [
|
|
|
228
258
|
},
|
|
229
259
|
{
|
|
230
260
|
name: 'player_stats',
|
|
231
|
-
outcome: 'Tennis career statistics: W/L, win%, titles, per-surface
|
|
261
|
+
outcome: 'Tennis career statistics: singles W/L, win%, titles, per-surface and per-level breakdowns, serving stats, doubles and combined scopes, and named gaps',
|
|
232
262
|
parallelSafe: true,
|
|
233
263
|
games: ['tennis'],
|
|
234
264
|
jobs: ['player_form'],
|
|
@@ -429,7 +459,7 @@ const TOOL_CATALOG = [
|
|
|
429
459
|
games: ['cs2'],
|
|
430
460
|
jobs: ['team_page', 'preview'],
|
|
431
461
|
exampleArgs: { teamId: 'cs2-team-4608', days: 90 },
|
|
432
|
-
preferOver: ['manual match history
|
|
462
|
+
preferOver: ['manual match history lookups'],
|
|
433
463
|
doNotUse: 'For non-CS2 games',
|
|
434
464
|
},
|
|
435
465
|
{
|
|
@@ -459,7 +489,7 @@ const TOOL_CATALOG = [
|
|
|
459
489
|
games: ['cs2'],
|
|
460
490
|
jobs: ['match_page', 'preview'],
|
|
461
491
|
exampleArgs: { matchId: '2397603' },
|
|
462
|
-
preferOver: ['
|
|
492
|
+
preferOver: ['parsing match page text'],
|
|
463
493
|
doNotUse: 'For map scores → match_summary',
|
|
464
494
|
},
|
|
465
495
|
{
|
|
@@ -469,7 +499,7 @@ const TOOL_CATALOG = [
|
|
|
469
499
|
games: ['cs2'],
|
|
470
500
|
jobs: ['team_page', 'schedule'],
|
|
471
501
|
exampleArgs: { limit: 20 },
|
|
472
|
-
preferOver: ['manual news
|
|
502
|
+
preferOver: ['manual news scanning'],
|
|
473
503
|
doNotUse: 'For current active rosters → team_profile',
|
|
474
504
|
},
|
|
475
505
|
{
|
|
@@ -493,6 +523,7 @@ const TOOL_CATALOG = [
|
|
|
493
523
|
doNotUse: 'Any job covered by a curated tool',
|
|
494
524
|
},
|
|
495
525
|
...COMBAT_CATALOG,
|
|
526
|
+
...WEBHOOK_CATALOG,
|
|
496
527
|
];
|
|
497
528
|
const JOBS = [
|
|
498
529
|
{ id: 'live_board', description: 'Ops/dashboard live strip', recommendedTools: ['live_matches', 'match_summary'] },
|
|
@@ -510,6 +541,7 @@ const JOBS = [
|
|
|
510
541
|
recommendedTools: ['event_card', 'player_profile', 'resolve_entity'],
|
|
511
542
|
},
|
|
512
543
|
{ id: 'app_scaffold', description: 'Design-time multi-screen prototype', recommendedTools: ['list_capabilities', 'api_health', 'live_matches'] },
|
|
544
|
+
{ id: 'webhooks', description: 'Set up, test and debug push delivery instead of polling', recommendedTools: ['explain_webhook_setup', 'list_webhook_events', 'create_webhook', 'test_webhook', 'webhook_deliveries', 'create_webhook_inbox'] },
|
|
513
545
|
];
|
|
514
546
|
const RECIPES = [
|
|
515
547
|
{
|
|
@@ -588,7 +620,7 @@ Prefer over: guessing from memory; exploring raw OpenAPI via call_api.
|
|
|
588
620
|
|
|
589
621
|
Do not use when: you already know the tool and have IDs — call that tool directly.
|
|
590
622
|
|
|
591
|
-
Parallel-safe: yes.
|
|
623
|
+
Parallel-safe: yes. API calls: 0.
|
|
592
624
|
Example: { "game": "cs2", "job": "live_board", "includeExamples": true }`,
|
|
593
625
|
inputSchema: {
|
|
594
626
|
type: 'object',
|
|
@@ -682,7 +714,7 @@ Prefer over: probing random game endpoints to test the key.
|
|
|
682
714
|
|
|
683
715
|
Do not use when: you only need live scores — use live_matches.
|
|
684
716
|
|
|
685
|
-
Parallel-safe: yes.
|
|
717
|
+
Parallel-safe: yes. API calls: 1–6.
|
|
686
718
|
Example: { "includeGameProbes": true }`,
|
|
687
719
|
inputSchema: {
|
|
688
720
|
type: 'object',
|
|
@@ -867,10 +899,13 @@ Prefer curated tools for all standard jobs (live, schedule, profiles, standings,
|
|
|
867
899
|
|
|
868
900
|
Do not use when: a curated tool covers the outcome. Avoid parallel storms; same plan rate limits apply.
|
|
869
901
|
|
|
902
|
+
Paths are relative to the API base, so "/tennis/players", not "/api/v1/tennis/players". Put query params in query or queryJson, not in the path.
|
|
903
|
+
Tennis: /tennis/h2h takes players=a,b, or player1_id and player2_id (one form is required). /tennis/players has limit up to 100 (over that is a 422; use /tennis/players/batch for a roster). /tennis/tournaments/calendar needs no year (default this year) and returns every row unless given limit. /tennis/rankings defaults to rank_max=100: pass rank_max=5000 for the whole list. /tennis/schedule returns the whole day unless given limit.
|
|
904
|
+
|
|
870
905
|
Path must start with / and match allowlisted prefixes: /health, /lol, /cs2, /dota2, /cod, /ufc, /fortnite, /apex, /tennis, /raf, /bkfc, /bellator, /pride, /strikeforce, /wec, /dream, /elitexc, /affliction, /pfl, /oktagon, /mma.
|
|
871
906
|
Rejects absolute URLs and path traversal → PATH_NOT_ALLOWED.
|
|
872
907
|
|
|
873
|
-
Parallel-safe: yes but discouraged in bulk.
|
|
908
|
+
Parallel-safe: yes but discouraged in bulk. API calls: 1.
|
|
874
909
|
Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"page\\":1,\\"limit\\":20}" }`,
|
|
875
910
|
inputSchema: {
|
|
876
911
|
type: 'object',
|
|
@@ -958,7 +993,7 @@ Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"pag
|
|
|
958
993
|
if (!res.ok) {
|
|
959
994
|
return errorEnvelope({
|
|
960
995
|
code: mapHttpToCode(res.status, { gameNotIncluded: gameNotIncludedHint(res.data) }),
|
|
961
|
-
message: `
|
|
996
|
+
message: `Cito API returned HTTP ${res.status} for ${method} ${path}`,
|
|
962
997
|
game: null,
|
|
963
998
|
source: 'call_api',
|
|
964
999
|
requestId,
|
|
@@ -987,7 +1022,7 @@ Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"pag
|
|
|
987
1022
|
},
|
|
988
1023
|
};
|
|
989
1024
|
const SPEC_OMITS = {
|
|
990
|
-
tennis: '
|
|
1025
|
+
tennis: 'Tennis routes (players, matches, h2h, rankings, tournaments, live) are under /tennis. Use the curated tools with game tennis, or call_api with /tennis/* paths.',
|
|
991
1026
|
lol: 'The published OpenAPI spec documents no /lol paths, but LoL routes exist and work. Use the curated LoL tools (live_matches, upcoming_schedule, team_profile, standings, player_profile); for raw access, /lol/* is allowlisted for call_api even though it is undocumented.',
|
|
992
1027
|
apex: 'The published OpenAPI spec documents no /apex paths, but the Apex Legends ALGS routes exist and work through call_api: '
|
|
993
1028
|
+ 'GET /apex/algs/events, /apex/algs/events/{seriesId} (+ /scores, /bans), /apex/algs/matches, /apex/algs/players (+ /search?q=), '
|
|
@@ -1009,7 +1044,7 @@ Do not use when: a curated tool already covers the outcome (call list_capabiliti
|
|
|
1009
1044
|
|
|
1010
1045
|
Note: the spec omits /lol entirely, though LoL routes work. Filtering by game=lol returns that caveat rather than an empty list.
|
|
1011
1046
|
|
|
1012
|
-
Parallel-safe: yes.
|
|
1047
|
+
Parallel-safe: yes. API calls: 1.
|
|
1013
1048
|
Example: { "game": "ufc", "q": "rankings" }`,
|
|
1014
1049
|
inputSchema: {
|
|
1015
1050
|
type: 'object',
|
package/dist/tools/normalize.js
CHANGED
|
@@ -10,7 +10,8 @@ function deriveProxiedImageUrl(rawUrl, game) {
|
|
|
10
10
|
catch {
|
|
11
11
|
return null;
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
const fighterImageHost = 'ufc' + '.com';
|
|
14
|
+
if (host !== fighterImageHost && !host.endsWith(`.${fighterImageHost}`))
|
|
14
15
|
return null;
|
|
15
16
|
return `${IMAGE_PROXY_BASE}/public/images/ufc/${Buffer.from(rawUrl, 'utf8').toString('base64url')}`;
|
|
16
17
|
}
|
|
@@ -558,7 +559,7 @@ export function normalizeMatch(game, row, forcedStatus) {
|
|
|
558
559
|
? { sets: tennisSets, gameScore: tennisGameScore, server: tennisServer, currentSet: tennisCurrentSet }
|
|
559
560
|
: {}),
|
|
560
561
|
...(game === 'tennis' && typeof r.is_simulated === 'boolean'
|
|
561
|
-
? { isSimulated: r.is_simulated
|
|
562
|
+
? { isSimulated: r.is_simulated }
|
|
562
563
|
: {}),
|
|
563
564
|
...(isCancelled
|
|
564
565
|
? { cancelled: { isCancelled: true, reason: pickString(r.cancellationReason) ?? null } }
|
package/dist/tools/player.js
CHANGED
|
@@ -40,6 +40,25 @@ function identityFrom(game, raw, idHint, slugHint) {
|
|
|
40
40
|
attribution: pickString(r.portrait_attribution) ?? null,
|
|
41
41
|
sourceUrl: pickString(r.portrait_source_url) ?? null,
|
|
42
42
|
},
|
|
43
|
+
...(game === 'tennis' ? { tennis: tennisIdentity(r) } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function tennisIdentity(r) {
|
|
47
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
48
|
+
return {
|
|
49
|
+
tour: pickString(r.tour) ?? null,
|
|
50
|
+
currentRank: num(r.current_rank),
|
|
51
|
+
currentRankPoints: num(r.current_rank_points),
|
|
52
|
+
lastRank: num(r.last_rank),
|
|
53
|
+
lastRankDate: pickString(r.last_rank_date) ?? null,
|
|
54
|
+
careerHighRank: num(r.career_high_rank),
|
|
55
|
+
status: pickString(r.status) ?? null,
|
|
56
|
+
statusBasis: pickString(r.status_basis) ?? null,
|
|
57
|
+
retiredYear: num(r.retired_year),
|
|
58
|
+
lastMatchDate: pickString(r.last_match_date) ?? null,
|
|
59
|
+
isActive: typeof r.is_active === 'boolean' ? r.is_active : null,
|
|
60
|
+
hand: pickString(r.hand) ?? null,
|
|
61
|
+
isoAlpha2: pickString(r.iso_alpha2) ?? null,
|
|
43
62
|
};
|
|
44
63
|
}
|
|
45
64
|
export const playerProfile = {
|
|
@@ -54,7 +73,9 @@ Prefer over: manual multi-call career/trends/matches via call_api.
|
|
|
54
73
|
|
|
55
74
|
Do not use when: full team roster needed → team_profile; unresolved name → resolve_entity first.
|
|
56
75
|
|
|
57
|
-
|
|
76
|
+
Tennis: player.tennis.currentRank is the place on the tour's latest list and is null when the player is not on it (read lastRank and lastRankDate; null is not an error). player.tennis.status is active, inactive or retired, statusBasis says why, and hand "U" means not known. career.career_summary and career.surface_breakdown count singles only; career_summary.doubles and career_summary.combined hold the other scopes.
|
|
77
|
+
|
|
78
|
+
Parallel-safe: yes. API calls: 2–5.
|
|
58
79
|
Example: { "game": "cs2", "playerId": "cs2-player-1", "recentLimit": 10, "includeTrends": true }`,
|
|
59
80
|
inputSchema: {
|
|
60
81
|
type: 'object',
|
|
@@ -551,9 +572,9 @@ Prefer over: player_profile (identity + career aggregates, no streak); raw form
|
|
|
551
572
|
|
|
552
573
|
Do not use when: career totals/titles → player_profile with game tennis (wires /stats); ranking deltas → rankings_movers.
|
|
553
574
|
|
|
554
|
-
Tennis-only. Limit defaults to 10 (max 50, matching the API).
|
|
575
|
+
Tennis-only. Limit defaults to 10 (max 50, matching the API). Rows arrive newest-first; the streak is the leading run of that order.
|
|
555
576
|
|
|
556
|
-
Parallel-safe: yes.
|
|
577
|
+
Parallel-safe: yes. API calls: 1.`,
|
|
557
578
|
inputSchema: {
|
|
558
579
|
type: 'object',
|
|
559
580
|
additionalProperties: false,
|
|
@@ -713,7 +734,7 @@ Do not use when: aggregate totals, titles, or win% → player_stats; identity
|
|
|
713
734
|
Tennis-only. Limit defaults to 20 (max 50). Rows arrive newest-first. Use
|
|
714
735
|
surface to narrow to Hard/Clay/Grass; tournamentId pins one event.
|
|
715
736
|
|
|
716
|
-
Parallel-safe: yes.
|
|
737
|
+
Parallel-safe: yes. API calls: 1.`,
|
|
717
738
|
inputSchema: {
|
|
718
739
|
type: 'object',
|
|
719
740
|
additionalProperties: false,
|
|
@@ -880,7 +901,9 @@ Prefer over: player_profile (identity + a summary block, no breakdowns); player_
|
|
|
880
901
|
Do not use when: the match log → player_matches; ranking over time → player_rankings_history.
|
|
881
902
|
|
|
882
903
|
Tennis-only. Filters are surface (Hard/Clay/Grass/Carpet) and yearFrom/yearTo.
|
|
883
|
-
|
|
904
|
+
|
|
905
|
+
careerSummary and bySurface count singles only, at every level held (Challenger and ITF, walkovers and qualifying included), so they read higher than tour-level career records. careerSummary.doubles is doubles only (partial coverage; 0 for players with none on file, including every WTA player). careerSummary.combined is singles plus doubles where the player is listed first. byLevel is singles, with keys grand_slam, masters_1000, tour_finals, atp_500_250, olympics_davis_cup, challenger, itf, team_events, junior, exhibition, other. prizeMoneyUsd is the career total where known, null when a filter is set.
|
|
906
|
+
API calls: 1.`,
|
|
884
907
|
inputSchema: {
|
|
885
908
|
type: 'object',
|
|
886
909
|
additionalProperties: false,
|
|
@@ -970,6 +993,19 @@ Upstream cost: 1.`,
|
|
|
970
993
|
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
971
994
|
const summary = asRecord(body.career_summary) ?? {};
|
|
972
995
|
const filters = asRecord(body.filters) ?? {};
|
|
996
|
+
const careerScope = (raw) => {
|
|
997
|
+
const s = asRecord(raw);
|
|
998
|
+
if (!s)
|
|
999
|
+
return null;
|
|
1000
|
+
return {
|
|
1001
|
+
matchesPlayed: num(s.matches_played),
|
|
1002
|
+
matchesWon: num(s.matches_won),
|
|
1003
|
+
matchesLost: num(s.matches_lost),
|
|
1004
|
+
winPercentage: num(s.win_percentage),
|
|
1005
|
+
titles: num(s.titles_count),
|
|
1006
|
+
grandSlamTitles: num(s.grand_slam_titles),
|
|
1007
|
+
};
|
|
1008
|
+
};
|
|
973
1009
|
const camel = (key) => key.replace(/_([a-z0-9])/g, (_, c) => c.toUpperCase());
|
|
974
1010
|
const flatNumeric = (value) => {
|
|
975
1011
|
const rec = asRecord(value);
|
|
@@ -1057,6 +1093,9 @@ Upstream cost: 1.`,
|
|
|
1057
1093
|
grandSlamTitles: num(summary.grand_slam_titles),
|
|
1058
1094
|
mastersTitles: num(summary.masters_titles),
|
|
1059
1095
|
finalsReached: num(summary.finals_reached),
|
|
1096
|
+
prizeMoneyUsd: num(summary.prize_money_usd),
|
|
1097
|
+
combined: careerScope(summary.combined),
|
|
1098
|
+
doubles: careerScope(summary.doubles),
|
|
1060
1099
|
},
|
|
1061
1100
|
bySurface,
|
|
1062
1101
|
byLevel,
|
package/dist/tools/rankings.js
CHANGED
|
@@ -60,6 +60,20 @@ function normalizeEdge(row) {
|
|
|
60
60
|
points: num(r.points),
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
+
export function normalizeCoverage(raw) {
|
|
64
|
+
const c = asRecord(raw);
|
|
65
|
+
if (!c)
|
|
66
|
+
return null;
|
|
67
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
68
|
+
return {
|
|
69
|
+
status: pickString(c.status) ?? null,
|
|
70
|
+
rankedPlayers: num(c.ranked_players),
|
|
71
|
+
deepestRank: num(c.deepest_rank),
|
|
72
|
+
missingRanks: num(c.missing_ranks),
|
|
73
|
+
completeThrough: num(c.complete_through),
|
|
74
|
+
note: pickString(c.note) ?? null,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
63
77
|
export const rankingsMovers = {
|
|
64
78
|
name: 'rankings_movers',
|
|
65
79
|
description: `Tennis ranking movers: biggest climbers/fallers between the latest ATP/WTA list and its predecessor, plus new entries and drop-outs.
|
|
@@ -73,9 +87,11 @@ Do not use when: current top-N snapshot → standings with game tennis; player h
|
|
|
73
87
|
|
|
74
88
|
Tennis-only. Direction up (climbers, default), down (fallers), or both (largest absolute change). Date pins an older ranking list (YYYY-MM-DD); default is the latest, which the API resolves past zero-diff cloned lists.
|
|
75
89
|
|
|
90
|
+
Some archived lists are held only in part (WTA 1975-86 above all). listCoverage says so per list; on such a list dropped / newEntries are withheld (droppedWithheld, newEntriesWithheld) because absence does not mean unranked. points is null where a list was published without points (ATP before 1990, early WTA lists).
|
|
91
|
+
|
|
76
92
|
The two lists are NOT always one week apart: previousDate is whatever the archive holds next, and the archive has holes. Read gapDays / comparisonWindow (and comparisonsAreWeekly) before describing a delta as weekly — a warning is raised whenever the gap exceeds ten days.
|
|
77
93
|
|
|
78
|
-
Parallel-safe: yes.
|
|
94
|
+
Parallel-safe: yes. API calls: 1.`,
|
|
79
95
|
inputSchema: {
|
|
80
96
|
type: 'object',
|
|
81
97
|
additionalProperties: false,
|
|
@@ -188,9 +204,14 @@ Parallel-safe: yes. Upstream cost: 1.`,
|
|
|
188
204
|
const newEntries = (Array.isArray(body.new_entries) ? body.new_entries : []).map(normalizeEdge);
|
|
189
205
|
const dropped = (Array.isArray(body.dropped) ? body.dropped : []).map(normalizeEdge);
|
|
190
206
|
const total = typeof body.total === 'number' ? body.total : items.length;
|
|
207
|
+
const coverage = asRecord(body.list_coverage) ?? null;
|
|
208
|
+
const droppedWithheld = body.dropped_withheld === true;
|
|
209
|
+
const newEntriesWithheld = body.new_entries_withheld === true;
|
|
191
210
|
const rankingDate = pickString(body.ranking_date) ?? null;
|
|
192
211
|
const previousDate = pickString(body.previous_date) ?? null;
|
|
193
212
|
const gapDays = (() => {
|
|
213
|
+
if (typeof body.gap_days === 'number')
|
|
214
|
+
return body.gap_days;
|
|
194
215
|
if (!rankingDate || !previousDate)
|
|
195
216
|
return null;
|
|
196
217
|
const a = Date.parse(rankingDate);
|
|
@@ -205,6 +226,10 @@ Parallel-safe: yes. Upstream cost: 1.`,
|
|
|
205
226
|
? 'week-over-week'
|
|
206
227
|
: `${Math.round(gapDays / 7)}-week gap`;
|
|
207
228
|
const warnings = [];
|
|
229
|
+
if (droppedWithheld || newEntriesWithheld) {
|
|
230
|
+
warnings.push(pickString(body.coverage_note) ??
|
|
231
|
+
'A compared list is held only in part, so drop-outs / new entries are withheld: absence from it does not mean unranked.');
|
|
232
|
+
}
|
|
208
233
|
if (gapDays !== null && gapDays > 10) {
|
|
209
234
|
warnings.push(`The compared lists are ${gapDays} days apart (${previousDate} -> ${rankingDate}), not one week. ` +
|
|
210
235
|
`Every "movement" here accumulated over ${Math.round(gapDays / 7)} weeks; there is no published ` +
|
|
@@ -239,6 +264,14 @@ Parallel-safe: yes. Upstream cost: 1.`,
|
|
|
239
264
|
items,
|
|
240
265
|
newEntries,
|
|
241
266
|
dropped,
|
|
267
|
+
droppedWithheld,
|
|
268
|
+
newEntriesWithheld,
|
|
269
|
+
listCoverage: coverage
|
|
270
|
+
? {
|
|
271
|
+
current: normalizeCoverage(coverage.current),
|
|
272
|
+
previous: normalizeCoverage(coverage.previous),
|
|
273
|
+
}
|
|
274
|
+
: null,
|
|
242
275
|
total,
|
|
243
276
|
},
|
|
244
277
|
});
|
|
@@ -262,7 +295,7 @@ pages itself. hasMore and oldestReturned/newestReturned say exactly which window
|
|
|
262
295
|
back, omittedOlder counts the rows left at the old end and omittedNewer the rows on
|
|
263
296
|
earlier (newer) pages.
|
|
264
297
|
|
|
265
|
-
Parallel-safe: yes.
|
|
298
|
+
Parallel-safe: yes. API calls: 1.`,
|
|
266
299
|
inputSchema: {
|
|
267
300
|
type: 'object',
|
|
268
301
|
additionalProperties: false,
|
package/dist/tools/resolve.js
CHANGED
|
@@ -31,7 +31,7 @@ function pushCandidates(out, game, type, rows, q, limit) {
|
|
|
31
31
|
let matchedBy;
|
|
32
32
|
if (q && score <= 0 && index === 0) {
|
|
33
33
|
score = 95;
|
|
34
|
-
matchedBy = '
|
|
34
|
+
matchedBy = 'top-hit';
|
|
35
35
|
}
|
|
36
36
|
const secondary = {};
|
|
37
37
|
for (const key of ['lolPlayerId', 'codPlayerId', 'matchId', 'boutId', 'tournamentId', 'eventId']) {
|
|
@@ -366,7 +366,7 @@ async function searchGame(ctx, game, q, type, limit) {
|
|
|
366
366
|
candidates,
|
|
367
367
|
calls,
|
|
368
368
|
error: partialFromRejection(`game:${game}`, {
|
|
369
|
-
code: '
|
|
369
|
+
code: 'SERVICE_ERROR',
|
|
370
370
|
message: e.message,
|
|
371
371
|
}),
|
|
372
372
|
};
|
|
@@ -389,7 +389,7 @@ Do not use when: you already have a stable id/slug from a prior tool.
|
|
|
389
389
|
|
|
390
390
|
Empty/ambiguous results still return ok:true with best=null or needsDisambiguation=true — pick from candidates or refine q/game/type. Does not emit AMBIGUOUS_ENTITY as a hard error.
|
|
391
391
|
|
|
392
|
-
Parallel-safe: yes.
|
|
392
|
+
Parallel-safe: yes. API calls: 1–5.
|
|
393
393
|
Example: { "q": "T1", "game": "lol", "type": "team", "limit": 5 }`,
|
|
394
394
|
inputSchema: {
|
|
395
395
|
type: 'object',
|
|
@@ -507,7 +507,7 @@ Do not use when: fetching a known entity profile — use team_profile or player_
|
|
|
507
507
|
|
|
508
508
|
UFC: with q set, results are ranked (exact name > multi-token match > nickname). "Jon Jones" should return jon-jones first — never the generic P4P list.
|
|
509
509
|
|
|
510
|
-
Parallel-safe: yes.
|
|
510
|
+
Parallel-safe: yes. API calls: 1–3.
|
|
511
511
|
Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
512
512
|
inputSchema: {
|
|
513
513
|
type: 'object',
|
|
@@ -792,11 +792,11 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
792
792
|
meta: {
|
|
793
793
|
...(it.meta ?? {}),
|
|
794
794
|
score: raw,
|
|
795
|
-
...(isProtected && raw < RELEVANCE_FLOOR ? { matchedBy: '
|
|
795
|
+
...(isProtected && raw < RELEVANCE_FLOOR ? { matchedBy: 'top-hit' } : {}),
|
|
796
796
|
},
|
|
797
797
|
};
|
|
798
798
|
});
|
|
799
|
-
const keep = (it) => it.meta?.score >= RELEVANCE_FLOOR || it.meta?.matchedBy === '
|
|
799
|
+
const keep = (it) => it.meta?.score >= RELEVANCE_FLOOR || it.meta?.matchedBy === 'top-hit';
|
|
800
800
|
droppedBelowFloor = items
|
|
801
801
|
.filter((it) => !keep(it))
|
|
802
802
|
.map((it) => ({ id: it.id, name: it.name, score: it.meta?.score }));
|
|
@@ -805,8 +805,8 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
805
805
|
}
|
|
806
806
|
if (q && !preserveUpstreamOrder) {
|
|
807
807
|
items.sort((a, b) => {
|
|
808
|
-
const am = a.meta?.matchedBy === '
|
|
809
|
-
const bm = b.meta?.matchedBy === '
|
|
808
|
+
const am = a.meta?.matchedBy === 'top-hit' ? 1 : 0;
|
|
809
|
+
const bm = b.meta?.matchedBy === 'top-hit' ? 1 : 0;
|
|
810
810
|
return bm - am || scoreOf(b) - scoreOf(a) || a.name.localeCompare(b.name);
|
|
811
811
|
});
|
|
812
812
|
}
|
|
@@ -820,8 +820,8 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
820
820
|
.slice(0, 5)
|
|
821
821
|
.map((d) => `${d.name} (${d.id})`)
|
|
822
822
|
.join(', ');
|
|
823
|
-
floorWarnings.push(`Dropped ${droppedBelowFloor.length} result(s) that do not match "${q}" at all
|
|
824
|
-
`substring match with no relevance floor, so it pads every page: ${sample}. ` +
|
|
823
|
+
floorWarnings.push(`Dropped ${droppedBelowFloor.length} result(s) that do not match "${q}" at all (the name search is a ` +
|
|
824
|
+
`substring match with no relevance floor, so it pads every page): ${sample}. ` +
|
|
825
825
|
`If one of these is what you wanted, query it by name or id directly.`);
|
|
826
826
|
}
|
|
827
827
|
return successEnvelope({
|
|
@@ -849,7 +849,7 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
|
849
849
|
matched: items.length,
|
|
850
850
|
droppedBelowFloor: droppedBelowFloor.length,
|
|
851
851
|
dropped: droppedBelowFloor.slice(0, 25),
|
|
852
|
-
|
|
852
|
+
unfilteredTotal: upstreamTotal ?? null,
|
|
853
853
|
},
|
|
854
854
|
}
|
|
855
855
|
: {}),
|
package/dist/tools/schedule.js
CHANGED
|
@@ -54,7 +54,7 @@ export const tennisSchedule = {
|
|
|
54
54
|
When to use:
|
|
55
55
|
- "What tennis is on today?"; a specific day's fixtures; results completed on a given date; building a daily digest.
|
|
56
56
|
|
|
57
|
-
Prefer over: call_api for /tennis/schedule and /tennis/matches/completed (
|
|
57
|
+
Prefer over: call_api for /tennis/schedule and /tennis/matches/completed (this tool supplies and validates the date, and bounds the rows); live_matches (in-progress only, right now); upcoming_schedule (a forward window, not a specific day).
|
|
58
58
|
|
|
59
59
|
Do not use when: matches currently in play → live_matches; a named player's history → player_matches.
|
|
60
60
|
|
|
@@ -62,13 +62,14 @@ Tennis-only. 'date' defaults to today (UTC) and MUST be YYYY-MM-DD. Choose with
|
|
|
62
62
|
'include': both (default), scheduled, or completed. Both halves are fetched in
|
|
63
63
|
parallel, so either can fail on its own without losing the other.
|
|
64
64
|
|
|
65
|
-
'limit' bounds EACH half (the
|
|
66
|
-
bound is applied here); scheduledCount/completedCount report how many rows the
|
|
65
|
+
'limit' bounds EACH half (the bound is applied by this tool); scheduledCount/completedCount report how many rows the
|
|
67
66
|
day held and *Returned how many came back. The two halves overlap: the day's
|
|
68
67
|
list includes matches already played, so the same matchId can appear in both —
|
|
69
68
|
see the overlap block before de-duplicating or counting.
|
|
70
69
|
|
|
71
|
-
|
|
70
|
+
Rows: start_time_known false means no start time is held (scheduled_at is then midnight). start_time_kind says whether scheduled_at is when the match actually started (actual) or the published order-of-play time (scheduled). Read status for where a match is now and outcome for how it ended.
|
|
71
|
+
|
|
72
|
+
Parallel-safe: yes. API calls: 1 or 2.`,
|
|
72
73
|
inputSchema: {
|
|
73
74
|
type: 'object',
|
|
74
75
|
additionalProperties: false,
|
|
@@ -172,7 +173,7 @@ Parallel-safe: yes. Upstream cost: 1 or 2.`,
|
|
|
172
173
|
else if (schedRes.status === 'rejected') {
|
|
173
174
|
rejected.push({
|
|
174
175
|
section: 'schedule',
|
|
175
|
-
code: '
|
|
176
|
+
code: 'SERVICE_ERROR',
|
|
176
177
|
message: String(schedRes.reason).slice(0, 200),
|
|
177
178
|
});
|
|
178
179
|
}
|
|
@@ -194,7 +195,7 @@ Parallel-safe: yes. Upstream cost: 1 or 2.`,
|
|
|
194
195
|
else if (compRes.status === 'rejected') {
|
|
195
196
|
rejected.push({
|
|
196
197
|
section: 'completed',
|
|
197
|
-
code: '
|
|
198
|
+
code: 'SERVICE_ERROR',
|
|
198
199
|
message: String(compRes.reason).slice(0, 200),
|
|
199
200
|
});
|
|
200
201
|
}
|