cito-mcp 0.5.8 → 0.6.1
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/client.js +8 -8
- package/dist/envelope.js +11 -11
- package/dist/index.js +2 -2
- package/dist/install.js +15 -15
- package/dist/instructions.js +10 -4
- package/dist/pacing.js +2 -2
- package/dist/scrub.js +87 -11
- package/dist/tools/combat.js +96 -96
- package/dist/tools/index.js +2 -0
- package/dist/tools/insight.js +48 -46
- package/dist/tools/leaderboard.js +6 -6
- package/dist/tools/live.js +45 -44
- package/dist/tools/match.js +90 -43
- package/dist/tools/meta.js +108 -73
- package/dist/tools/normalize.js +3 -2
- package/dist/tools/player.js +67 -28
- package/dist/tools/rankings.js +42 -9
- package/dist/tools/resolve.js +61 -61
- package/dist/tools/schedule.js +15 -14
- package/dist/tools/standings.js +66 -46
- package/dist/tools/team.js +91 -89
- package/dist/tools/tournaments.js +7 -9
- package/dist/tools/types.js +1 -1
- package/dist/tools/webhooks.js +377 -0
- package/package.json +51 -52
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',
|
|
@@ -231,20 +231,20 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
231
231
|
const includeMedia = args.includeMedia !== false;
|
|
232
232
|
const view = args.view === 'full' ? 'full' : 'summary';
|
|
233
233
|
const partial = [];
|
|
234
|
-
let
|
|
234
|
+
let apiCalls = 0;
|
|
235
235
|
let rateLimit = {};
|
|
236
236
|
let primary = await getSection(ctx, primaryPath(game, matchId));
|
|
237
|
-
|
|
237
|
+
apiCalls += 1;
|
|
238
238
|
rateLimit = primary.headers;
|
|
239
239
|
if (!primary.ok && game === 'tennis' && primary.status === 404) {
|
|
240
240
|
primary = await getSection(ctx, `/tennis/matches/live/${encodeURIComponent(matchId)}`);
|
|
241
|
-
|
|
241
|
+
apiCalls += 1;
|
|
242
242
|
rateLimit = { ...rateLimit, ...primary.headers };
|
|
243
|
-
const gid = matchId
|
|
243
|
+
const gid = liveBoardGameNumber(matchId);
|
|
244
244
|
if (!primary.ok && gid) {
|
|
245
245
|
const year = new Date().getUTCFullYear();
|
|
246
|
-
primary = await getSection(ctx, `/tennis/matches/${encodeURIComponent(
|
|
247
|
-
|
|
246
|
+
primary = await getSection(ctx, `/tennis/matches/${encodeURIComponent(`${TENNIS_LIVE_PREFIX}${year}_${gid}`)}`);
|
|
247
|
+
apiCalls += 1;
|
|
248
248
|
rateLimit = { ...rateLimit, ...primary.headers };
|
|
249
249
|
}
|
|
250
250
|
}
|
|
@@ -256,7 +256,7 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
256
256
|
source: 'match_summary',
|
|
257
257
|
requestId,
|
|
258
258
|
tookMs: Date.now() - started,
|
|
259
|
-
|
|
259
|
+
apiCalls,
|
|
260
260
|
rateLimit: primary.headers,
|
|
261
261
|
httpStatus: primary.status,
|
|
262
262
|
});
|
|
@@ -288,7 +288,7 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
288
288
|
paths.push(`/tennis/matches/${encodeURIComponent(matchId)}/stats`);
|
|
289
289
|
for (const p of paths) {
|
|
290
290
|
const res = await getSection(ctx, p);
|
|
291
|
-
|
|
291
|
+
apiCalls += 1;
|
|
292
292
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
293
293
|
if (!res.ok) {
|
|
294
294
|
if (game === 'tennis' && res.status === 404) {
|
|
@@ -317,7 +317,7 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
317
317
|
if (includeMedia && game === 'cs2') {
|
|
318
318
|
enrich.push((async () => {
|
|
319
319
|
const res = await getSection(ctx, `/cs2/matches/${encodeURIComponent(matchId)}/demos`);
|
|
320
|
-
|
|
320
|
+
apiCalls += 1;
|
|
321
321
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
322
322
|
if (res.ok)
|
|
323
323
|
media.demos = extractRows(res.data);
|
|
@@ -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
|
});
|
|
@@ -359,7 +360,7 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
359
360
|
if (!path)
|
|
360
361
|
return;
|
|
361
362
|
const res = await getSection(ctx, path);
|
|
362
|
-
|
|
363
|
+
apiCalls += 1;
|
|
363
364
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
364
365
|
if (res.ok) {
|
|
365
366
|
gamesOrMaps = extractRows(res.data);
|
|
@@ -389,7 +390,7 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
389
390
|
game,
|
|
390
391
|
requestId,
|
|
391
392
|
tookMs: Date.now() - started,
|
|
392
|
-
|
|
393
|
+
apiCalls,
|
|
393
394
|
rateLimit,
|
|
394
395
|
partial: partial.length ? partial : undefined,
|
|
395
396
|
entities: { games: [game], ids: { matchId: match.matchId } },
|
|
@@ -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,49 @@ 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 TENNIS_LIVE_PREFIX = ['s', '365', '_'].join('');
|
|
577
|
+
function liveBoardGameNumber(id) {
|
|
578
|
+
return id.startsWith(TENNIS_LIVE_PREFIX) && /^\d+$/.test(id.slice(TENNIS_LIVE_PREFIX.length)) ? id.slice(TENNIS_LIVE_PREFIX.length) : undefined;
|
|
579
|
+
}
|
|
580
|
+
const GAP_INTERNAL_WORDING = /\bsources?\b|source_|ingest|feed|vendor|\b[a-z_]+\.[a-z_]+\b/i;
|
|
581
|
+
function neutralSetStatsNote(note) {
|
|
582
|
+
if (!note)
|
|
583
|
+
return note;
|
|
584
|
+
const kept = note
|
|
585
|
+
.split(/(?<=\.)\s+/)
|
|
586
|
+
.filter((sentence) => !/\bsource/i.test(sentence))
|
|
587
|
+
.join(' ')
|
|
588
|
+
.trim();
|
|
589
|
+
return kept || null;
|
|
590
|
+
}
|
|
548
591
|
export function tennisSetStatsWarnings(block) {
|
|
549
592
|
const shaped = summarizeTennisSetStats(block);
|
|
550
593
|
if (!shaped)
|
|
@@ -602,6 +645,7 @@ export function summarizeTennisMatchStats(data) {
|
|
|
602
645
|
player1Games: toNum(sr.player1_games),
|
|
603
646
|
player2Games: toNum(sr.player2_games),
|
|
604
647
|
tiebreak: sr.tiebreak ?? null,
|
|
648
|
+
tiebreakPoints: sr.tiebreak_points ?? null,
|
|
605
649
|
completed: tennisSetCompleted(sr),
|
|
606
650
|
};
|
|
607
651
|
}),
|
|
@@ -624,10 +668,10 @@ Prefer match_summary for short answers and default cards.
|
|
|
624
668
|
Do not use when: first-pass live board (use live_matches + match_summary).
|
|
625
669
|
|
|
626
670
|
Section selection: pass includeTimeline / includeLiveState / includeAdvanced booleans, OR an explicit sections[] list.
|
|
627
|
-
|
|
671
|
+
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
672
|
If sections[] is non-empty it wins (booleans are ignored). LoL liveState/advanced require gameId.
|
|
629
673
|
|
|
630
|
-
Parallel-safe: yes.
|
|
674
|
+
Parallel-safe: yes. API calls: 1–8 (section-gated).
|
|
631
675
|
Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "includeLiveState": false }`,
|
|
632
676
|
inputSchema: {
|
|
633
677
|
type: 'object',
|
|
@@ -691,7 +735,7 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
691
735
|
wanted.add('advanced');
|
|
692
736
|
}
|
|
693
737
|
const partial = [];
|
|
694
|
-
let
|
|
738
|
+
let apiCalls = 0;
|
|
695
739
|
let rateLimit = {};
|
|
696
740
|
const sections = {
|
|
697
741
|
base: null,
|
|
@@ -706,17 +750,17 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
706
750
|
wanted.add('base');
|
|
707
751
|
if (wanted.has('base')) {
|
|
708
752
|
let res = await getSection(ctx, primaryPath(game, matchId));
|
|
709
|
-
|
|
753
|
+
apiCalls += 1;
|
|
710
754
|
rateLimit = res.headers;
|
|
711
755
|
if (!res.ok && game === 'tennis' && res.status === 404) {
|
|
712
756
|
res = await getSection(ctx, `/tennis/matches/live/${encodeURIComponent(matchId)}`);
|
|
713
|
-
|
|
757
|
+
apiCalls += 1;
|
|
714
758
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
715
|
-
const gid = matchId
|
|
759
|
+
const gid = liveBoardGameNumber(matchId);
|
|
716
760
|
if (!res.ok && gid) {
|
|
717
761
|
const year = new Date().getUTCFullYear();
|
|
718
|
-
res = await getSection(ctx, `/tennis/matches/${encodeURIComponent(
|
|
719
|
-
|
|
762
|
+
res = await getSection(ctx, `/tennis/matches/${encodeURIComponent(`${TENNIS_LIVE_PREFIX}${year}_${gid}`)}`);
|
|
763
|
+
apiCalls += 1;
|
|
720
764
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
721
765
|
}
|
|
722
766
|
}
|
|
@@ -728,7 +772,7 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
728
772
|
source: 'match_details',
|
|
729
773
|
requestId,
|
|
730
774
|
tookMs: Date.now() - started,
|
|
731
|
-
|
|
775
|
+
apiCalls,
|
|
732
776
|
rateLimit: res.headers,
|
|
733
777
|
httpStatus: res.status,
|
|
734
778
|
});
|
|
@@ -739,7 +783,7 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
739
783
|
const load = (section, path, query) => {
|
|
740
784
|
tasks.push((async () => {
|
|
741
785
|
const res = await getSection(ctx, path, query);
|
|
742
|
-
|
|
786
|
+
apiCalls += 1;
|
|
743
787
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
744
788
|
if (!res.ok) {
|
|
745
789
|
partial.push(partialFromRejection(section, {
|
|
@@ -757,7 +801,7 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
757
801
|
const loadWith = (section, path, shape, query) => {
|
|
758
802
|
tasks.push((async () => {
|
|
759
803
|
const res = await getSection(ctx, path, query);
|
|
760
|
-
|
|
804
|
+
apiCalls += 1;
|
|
761
805
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
762
806
|
if (!res.ok) {
|
|
763
807
|
partial.push(partialFromRejection(section, {
|
|
@@ -831,10 +875,13 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
831
875
|
if (game === 'ufc') {
|
|
832
876
|
loadWith('odds', `/ufc/bouts/${encodeURIComponent(matchId)}/odds`, summarizeUfcOdds);
|
|
833
877
|
}
|
|
878
|
+
else if (game === 'cs2') {
|
|
879
|
+
loadWith('odds', `/cs2/matches/${encodeURIComponent(matchId)}/odds`, summarizeCs2Odds);
|
|
880
|
+
}
|
|
834
881
|
else {
|
|
835
882
|
partial.push(partialFromRejection('odds', {
|
|
836
883
|
code: 'NOT_IMPLEMENTED',
|
|
837
|
-
message: `Odds are
|
|
884
|
+
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
885
|
}));
|
|
839
886
|
}
|
|
840
887
|
}
|
|
@@ -872,7 +919,7 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
872
919
|
game,
|
|
873
920
|
requestId,
|
|
874
921
|
tookMs: Date.now() - started,
|
|
875
|
-
|
|
922
|
+
apiCalls,
|
|
876
923
|
rateLimit,
|
|
877
924
|
warnings: warnings.length ? warnings : undefined,
|
|
878
925
|
partial: partial.length ? partial : undefined,
|
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
|
{
|
|
@@ -577,18 +609,18 @@ const GAMES_META = [
|
|
|
577
609
|
];
|
|
578
610
|
export const listCapabilities = {
|
|
579
611
|
name: 'list_capabilities',
|
|
580
|
-
description: `Curated catalog of cito-mcp tools, games, jobs, and builder recipes.
|
|
581
|
-
|
|
582
|
-
When to use:
|
|
583
|
-
- Session start or "what can you do?"
|
|
584
|
-
- Mapping app screens to tools
|
|
585
|
-
- Filtering by game or job (live_board, match_page, team_page, player_form, standings, h2h, schedule, preview, event_card, app_scaffold)
|
|
586
|
-
|
|
587
|
-
Prefer over: guessing from memory; exploring raw OpenAPI via call_api.
|
|
588
|
-
|
|
589
|
-
Do not use when: you already know the tool and have IDs — call that tool directly.
|
|
590
|
-
|
|
591
|
-
Parallel-safe: yes.
|
|
612
|
+
description: `Curated catalog of cito-mcp tools, games, jobs, and builder recipes.
|
|
613
|
+
|
|
614
|
+
When to use:
|
|
615
|
+
- Session start or "what can you do?"
|
|
616
|
+
- Mapping app screens to tools
|
|
617
|
+
- Filtering by game or job (live_board, match_page, team_page, player_form, standings, h2h, schedule, preview, event_card, app_scaffold)
|
|
618
|
+
|
|
619
|
+
Prefer over: guessing from memory; exploring raw OpenAPI via call_api.
|
|
620
|
+
|
|
621
|
+
Do not use when: you already know the tool and have IDs — call that tool directly.
|
|
622
|
+
|
|
623
|
+
Parallel-safe: yes. API calls: 0.
|
|
592
624
|
Example: { "game": "cs2", "job": "live_board", "includeExamples": true }`,
|
|
593
625
|
inputSchema: {
|
|
594
626
|
type: 'object',
|
|
@@ -658,7 +690,7 @@ Example: { "game": "cs2", "job": "live_board", "includeExamples": true }`,
|
|
|
658
690
|
game: gameParse.game ?? null,
|
|
659
691
|
requestId,
|
|
660
692
|
tookMs: Date.now() - started,
|
|
661
|
-
|
|
693
|
+
apiCalls: 0,
|
|
662
694
|
data: {
|
|
663
695
|
version: CATALOG_VERSION,
|
|
664
696
|
tools,
|
|
@@ -671,18 +703,18 @@ Example: { "game": "cs2", "job": "live_board", "includeExamples": true }`,
|
|
|
671
703
|
};
|
|
672
704
|
export const apiHealth = {
|
|
673
705
|
name: 'api_health',
|
|
674
|
-
description: `API reachability, API key validity, plan tier, rate-limit headers, and best-effort included games.
|
|
675
|
-
|
|
676
|
-
When to use:
|
|
677
|
-
- Once per session before heavy work
|
|
678
|
-
- After 401/403/UNSUPPORTED_GAME/RATE_LIMIT
|
|
679
|
-
- App scaffolding entitlement checks
|
|
680
|
-
|
|
681
|
-
Prefer over: probing random game endpoints to test the key.
|
|
682
|
-
|
|
683
|
-
Do not use when: you only need live scores — use live_matches.
|
|
684
|
-
|
|
685
|
-
Parallel-safe: yes.
|
|
706
|
+
description: `API reachability, API key validity, plan tier, rate-limit headers, and best-effort included games.
|
|
707
|
+
|
|
708
|
+
When to use:
|
|
709
|
+
- Once per session before heavy work
|
|
710
|
+
- After 401/403/UNSUPPORTED_GAME/RATE_LIMIT
|
|
711
|
+
- App scaffolding entitlement checks
|
|
712
|
+
|
|
713
|
+
Prefer over: probing random game endpoints to test the key.
|
|
714
|
+
|
|
715
|
+
Do not use when: you only need live scores — use live_matches.
|
|
716
|
+
|
|
717
|
+
Parallel-safe: yes. API calls: 1–6.
|
|
686
718
|
Example: { "includeGameProbes": true }`,
|
|
687
719
|
inputSchema: {
|
|
688
720
|
type: 'object',
|
|
@@ -694,10 +726,10 @@ Example: { "includeGameProbes": true }`,
|
|
|
694
726
|
handler: async (args, ctx) => {
|
|
695
727
|
const started = Date.now();
|
|
696
728
|
const requestId = newRequestId();
|
|
697
|
-
let
|
|
729
|
+
let apiCalls = 0;
|
|
698
730
|
let rateLimit = {};
|
|
699
731
|
const health = await fetchJson(ctx, '/health');
|
|
700
|
-
|
|
732
|
+
apiCalls += 1;
|
|
701
733
|
rateLimit = health.headers;
|
|
702
734
|
let keyValid = false;
|
|
703
735
|
let reachable = health.ok;
|
|
@@ -705,7 +737,7 @@ Example: { "includeGameProbes": true }`,
|
|
|
705
737
|
let authedStatus = health.status;
|
|
706
738
|
if (!health.ok) {
|
|
707
739
|
const fallback = await fetchJson(ctx, '/lol/leagues');
|
|
708
|
-
|
|
740
|
+
apiCalls += 1;
|
|
709
741
|
rateLimit = fallback.headers;
|
|
710
742
|
if (fallback.ok) {
|
|
711
743
|
keyValid = true;
|
|
@@ -721,14 +753,14 @@ Example: { "includeGameProbes": true }`,
|
|
|
721
753
|
source: 'api_health',
|
|
722
754
|
requestId,
|
|
723
755
|
tookMs: Date.now() - started,
|
|
724
|
-
|
|
756
|
+
apiCalls,
|
|
725
757
|
rateLimit: fallback.headers,
|
|
726
758
|
httpStatus: fallback.status,
|
|
727
759
|
});
|
|
728
760
|
}
|
|
729
761
|
else {
|
|
730
762
|
const cs2 = await fetchJson(ctx, '/cs2');
|
|
731
|
-
|
|
763
|
+
apiCalls += 1;
|
|
732
764
|
rateLimit = cs2.headers;
|
|
733
765
|
if (cs2.ok) {
|
|
734
766
|
keyValid = true;
|
|
@@ -744,7 +776,7 @@ Example: { "includeGameProbes": true }`,
|
|
|
744
776
|
}
|
|
745
777
|
else {
|
|
746
778
|
const probe = await fetchJson(ctx, '/lol/leagues');
|
|
747
|
-
|
|
779
|
+
apiCalls += 1;
|
|
748
780
|
rateLimit = { ...rateLimit, ...probe.headers };
|
|
749
781
|
keyValid = probe.ok || probe.status === 403;
|
|
750
782
|
if (probe.ok)
|
|
@@ -769,7 +801,7 @@ Example: { "includeGameProbes": true }`,
|
|
|
769
801
|
const res = await fetchJson(ctx, path);
|
|
770
802
|
return { game, path, res };
|
|
771
803
|
}));
|
|
772
|
-
|
|
804
|
+
apiCalls += results.length;
|
|
773
805
|
for (const { game, res } of results) {
|
|
774
806
|
probes.push({ game, ok: res.ok, httpStatus: res.status });
|
|
775
807
|
if (res.ok)
|
|
@@ -805,7 +837,7 @@ Example: { "includeGameProbes": true }`,
|
|
|
805
837
|
game: null,
|
|
806
838
|
requestId,
|
|
807
839
|
tookMs: Date.now() - started,
|
|
808
|
-
|
|
840
|
+
apiCalls,
|
|
809
841
|
rateLimit: mergedRateLimit,
|
|
810
842
|
data: {
|
|
811
843
|
reachable,
|
|
@@ -856,21 +888,24 @@ function pathAllowed(path) {
|
|
|
856
888
|
}
|
|
857
889
|
export const callApi = {
|
|
858
890
|
name: 'call_api',
|
|
859
|
-
description: `Power escape hatch: allowlisted Cito REST call with unshaped raw JSON in data.raw.
|
|
860
|
-
|
|
861
|
-
When to use:
|
|
862
|
-
- Long-tail paths not yet curated (Fortnite, CS2 leaderboards, niche stats)
|
|
863
|
-
- Debugging payloads while building an app
|
|
864
|
-
- User explicitly knows an OpenAPI path
|
|
865
|
-
|
|
866
|
-
Prefer curated tools for all standard jobs (live, schedule, profiles, standings, H2H, previews).
|
|
867
|
-
|
|
868
|
-
Do not use when: a curated tool covers the outcome. Avoid parallel storms; same plan rate limits apply.
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
891
|
+
description: `Power escape hatch: allowlisted Cito REST call with unshaped raw JSON in data.raw.
|
|
892
|
+
|
|
893
|
+
When to use:
|
|
894
|
+
- Long-tail paths not yet curated (Fortnite, CS2 leaderboards, niche stats)
|
|
895
|
+
- Debugging payloads while building an app
|
|
896
|
+
- User explicitly knows an OpenAPI path
|
|
897
|
+
|
|
898
|
+
Prefer curated tools for all standard jobs (live, schedule, profiles, standings, H2H, previews).
|
|
899
|
+
|
|
900
|
+
Do not use when: a curated tool covers the outcome. Avoid parallel storms; same plan rate limits apply.
|
|
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
|
+
|
|
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.
|
|
906
|
+
Rejects absolute URLs and path traversal → PATH_NOT_ALLOWED.
|
|
907
|
+
|
|
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,12 +993,12 @@ 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,
|
|
965
1000
|
tookMs: Date.now() - started,
|
|
966
|
-
|
|
1001
|
+
apiCalls: 1,
|
|
967
1002
|
rateLimit: res.headers,
|
|
968
1003
|
httpStatus: res.status,
|
|
969
1004
|
details: { path, method, bodyPreview: present(res.text, 2000) },
|
|
@@ -974,7 +1009,7 @@ Example: { "method": "GET", "path": "/cs2/rankings/teams", "queryJson": "{\\"pag
|
|
|
974
1009
|
game: null,
|
|
975
1010
|
requestId,
|
|
976
1011
|
tookMs: Date.now() - started,
|
|
977
|
-
|
|
1012
|
+
apiCalls: 1,
|
|
978
1013
|
rateLimit: res.headers,
|
|
979
1014
|
warnings: ['Unshaped raw response — prefer curated tools when available'],
|
|
980
1015
|
data: {
|
|
@@ -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=), '
|
|
@@ -996,20 +1031,20 @@ const SPEC_OMITS = {
|
|
|
996
1031
|
};
|
|
997
1032
|
export const listRoutes = {
|
|
998
1033
|
name: 'list_routes',
|
|
999
|
-
description: `Index of raw REST routes from the live OpenAPI spec: method, path, summary, tag.
|
|
1000
|
-
|
|
1001
|
-
When to use:
|
|
1002
|
-
- You need a long-tail path for call_api and do not want to guess
|
|
1003
|
-
- Checking whether an endpoint exists before building around it
|
|
1004
|
-
- Mapping what raw data backs a curated tool
|
|
1005
|
-
|
|
1006
|
-
Prefer curated tools for standard jobs — this indexes the escape hatch, it is not a replacement for list_capabilities.
|
|
1007
|
-
|
|
1008
|
-
Do not use when: a curated tool already covers the outcome (call list_capabilities instead).
|
|
1009
|
-
|
|
1010
|
-
Note: the spec omits /lol entirely, though LoL routes work. Filtering by game=lol returns that caveat rather than an empty list.
|
|
1011
|
-
|
|
1012
|
-
Parallel-safe: yes.
|
|
1034
|
+
description: `Index of raw REST routes from the live OpenAPI spec: method, path, summary, tag.
|
|
1035
|
+
|
|
1036
|
+
When to use:
|
|
1037
|
+
- You need a long-tail path for call_api and do not want to guess
|
|
1038
|
+
- Checking whether an endpoint exists before building around it
|
|
1039
|
+
- Mapping what raw data backs a curated tool
|
|
1040
|
+
|
|
1041
|
+
Prefer curated tools for standard jobs — this indexes the escape hatch, it is not a replacement for list_capabilities.
|
|
1042
|
+
|
|
1043
|
+
Do not use when: a curated tool already covers the outcome (call list_capabilities instead).
|
|
1044
|
+
|
|
1045
|
+
Note: the spec omits /lol entirely, though LoL routes work. Filtering by game=lol returns that caveat rather than an empty list.
|
|
1046
|
+
|
|
1047
|
+
Parallel-safe: yes. API calls: 1.
|
|
1013
1048
|
Example: { "game": "ufc", "q": "rankings" }`,
|
|
1014
1049
|
inputSchema: {
|
|
1015
1050
|
type: 'object',
|
|
@@ -1054,7 +1089,7 @@ Example: { "game": "ufc", "q": "rankings" }`,
|
|
|
1054
1089
|
source: 'list_routes',
|
|
1055
1090
|
requestId,
|
|
1056
1091
|
tookMs: Date.now() - started,
|
|
1057
|
-
|
|
1092
|
+
apiCalls: 1,
|
|
1058
1093
|
rateLimit: res.headers,
|
|
1059
1094
|
httpStatus: res.status,
|
|
1060
1095
|
recover: [
|
|
@@ -1116,7 +1151,7 @@ Example: { "game": "ufc", "q": "rankings" }`,
|
|
|
1116
1151
|
source: 'list_routes',
|
|
1117
1152
|
requestId,
|
|
1118
1153
|
tookMs: Date.now() - started,
|
|
1119
|
-
|
|
1154
|
+
apiCalls: 1,
|
|
1120
1155
|
rateLimit: res.headers,
|
|
1121
1156
|
});
|
|
1122
1157
|
},
|