cito-mcp 0.3.11 → 0.3.12
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/dist/tools/match.js +133 -2
- package/package.json +1 -1
package/dist/tools/match.js
CHANGED
|
@@ -389,6 +389,102 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
389
389
|
});
|
|
390
390
|
},
|
|
391
391
|
};
|
|
392
|
+
/**
|
|
393
|
+
* Fold a UFC bout's raw odds payload into something an agent can read.
|
|
394
|
+
*
|
|
395
|
+
* The REST response is deliberately complete: every bookmaker, every market, up
|
|
396
|
+
* to roughly 950 outcomes for a single bout. Handing that back whole would bury
|
|
397
|
+
* the two numbers a caller almost always wants, and the server's own
|
|
398
|
+
* instructions say not to flood odds. So the moneyline is summarised per fighter
|
|
399
|
+
* and every other market is reduced to a count, with the raw endpoint named so
|
|
400
|
+
* anyone who genuinely needs the full book can go and get it.
|
|
401
|
+
*
|
|
402
|
+
* isAvailable is carried through as currentlyOffered rather than filtered on. It
|
|
403
|
+
* means "a book is offering this right now", so a finished fight's closing line
|
|
404
|
+
* comes back with it false, and that is information rather than absence.
|
|
405
|
+
* Filtering here would recreate the bug where whole settled cards looked like
|
|
406
|
+
* they had no odds at all.
|
|
407
|
+
*/
|
|
408
|
+
export function summarizeUfcOdds(data) {
|
|
409
|
+
// fetchJson hands back the whole REST envelope ({ success, data, meta }), not
|
|
410
|
+
// the inner payload, so unwrap one level when it is there. Accepting both
|
|
411
|
+
// shapes because the unit tests feed the inner object directly; reading only
|
|
412
|
+
// the outer one returned a perfectly shaped, perfectly empty summary, which
|
|
413
|
+
// looked like "this bout has no odds" rather than like a bug.
|
|
414
|
+
const root = (data ?? {});
|
|
415
|
+
const payload = (root.data && typeof root.data === 'object' && !Array.isArray(root.data)
|
|
416
|
+
? root.data
|
|
417
|
+
: root);
|
|
418
|
+
const markets = Array.isArray(payload.markets) ? payload.markets : [];
|
|
419
|
+
const byFighter = new Map();
|
|
420
|
+
const otherMarkets = new Map();
|
|
421
|
+
let observedAt = null;
|
|
422
|
+
for (const market of markets) {
|
|
423
|
+
const outcomes = Array.isArray(market.outcomes) ? market.outcomes : [];
|
|
424
|
+
for (const outcome of outcomes) {
|
|
425
|
+
const observed = typeof outcome.observedAt === 'string' ? outcome.observedAt : null;
|
|
426
|
+
if (observed && (!observedAt || observed > observedAt))
|
|
427
|
+
observedAt = observed;
|
|
428
|
+
const fighter = (outcome.fighter ?? {});
|
|
429
|
+
const bookmaker = (outcome.bookmaker ?? {});
|
|
430
|
+
const slug = typeof fighter.slug === 'string' ? fighter.slug : null;
|
|
431
|
+
const type = typeof outcome.marketType === 'string' ? outcome.marketType : 'unknown';
|
|
432
|
+
const book = typeof bookmaker.slug === 'string' ? bookmaker.slug : 'unknown';
|
|
433
|
+
if (type === 'moneyline' && slug) {
|
|
434
|
+
const entry = byFighter.get(slug) ?? {
|
|
435
|
+
slug,
|
|
436
|
+
name: typeof fighter.name === 'string' ? fighter.name : null,
|
|
437
|
+
side: typeof outcome.side === 'string' ? outcome.side : null,
|
|
438
|
+
prices: [],
|
|
439
|
+
books: new Set(),
|
|
440
|
+
live: false,
|
|
441
|
+
};
|
|
442
|
+
if (typeof outcome.moneyline === 'number')
|
|
443
|
+
entry.prices.push(outcome.moneyline);
|
|
444
|
+
entry.books.add(book);
|
|
445
|
+
if (outcome.isAvailable === true)
|
|
446
|
+
entry.live = true;
|
|
447
|
+
byFighter.set(slug, entry);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const other = otherMarkets.get(type) ?? { outcomes: 0, books: new Set() };
|
|
451
|
+
other.outcomes += 1;
|
|
452
|
+
other.books.add(book);
|
|
453
|
+
otherMarkets.set(type, other);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// American odds: the best price is the highest number both for an underdog
|
|
457
|
+
// (+250 beats +180) and for a favourite (-110 beats -200), so it is simply the
|
|
458
|
+
// maximum in both cases.
|
|
459
|
+
const moneyline = [...byFighter.values()].map((entry) => {
|
|
460
|
+
const sorted = [...entry.prices].sort((a, b) => a - b);
|
|
461
|
+
const mid = sorted.length
|
|
462
|
+
? sorted.length % 2
|
|
463
|
+
? sorted[(sorted.length - 1) / 2]
|
|
464
|
+
: Math.round((sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2)
|
|
465
|
+
: null;
|
|
466
|
+
return {
|
|
467
|
+
fighter: { slug: entry.slug, name: entry.name },
|
|
468
|
+
side: entry.side,
|
|
469
|
+
bookmakers: entry.books.size,
|
|
470
|
+
bestAmerican: sorted.length ? sorted[sorted.length - 1] : null,
|
|
471
|
+
medianAmerican: mid,
|
|
472
|
+
impliedProbability: mid === null ? null : Number((mid > 0 ? 100 / (mid + 100) : -mid / (-mid + 100)).toFixed(4)),
|
|
473
|
+
currentlyOffered: entry.live,
|
|
474
|
+
};
|
|
475
|
+
});
|
|
476
|
+
return {
|
|
477
|
+
moneyline,
|
|
478
|
+
otherMarkets: [...otherMarkets.entries()]
|
|
479
|
+
.map(([type, value]) => ({ type, outcomes: value.outcomes, bookmakers: value.books.size }))
|
|
480
|
+
.sort((a, b) => b.outcomes - a.outcomes),
|
|
481
|
+
observedAt,
|
|
482
|
+
notes: moneyline.some((row) => !row.currentlyOffered)
|
|
483
|
+
? 'currentlyOffered=false means the market has closed; these are closing lines, not live prices.'
|
|
484
|
+
: undefined,
|
|
485
|
+
fullBookPath: '/ufc/bouts/{boutId}/odds',
|
|
486
|
+
};
|
|
487
|
+
}
|
|
392
488
|
export const matchDetails = {
|
|
393
489
|
name: 'match_details',
|
|
394
490
|
description: `Deep match package: optional timelines, advanced stats, live state/snapshots, full map/game tree, media inventory.
|
|
@@ -404,6 +500,7 @@ Prefer match_summary for short answers and default cards.
|
|
|
404
500
|
Do not use when: first-pass live board (use live_matches + match_summary).
|
|
405
501
|
|
|
406
502
|
Section selection: pass includeTimeline / includeLiveState / includeAdvanced booleans, OR an explicit sections[] list.
|
|
503
|
+
UFC betting lines: sections:["odds"] (opt-in, never in the default set).
|
|
407
504
|
If sections[] is non-empty it wins (booleans are ignored). LoL liveState/advanced require gameId.
|
|
408
505
|
|
|
409
506
|
Parallel-safe: yes. Upstream cost: 1–8 (section-gated).
|
|
@@ -420,9 +517,9 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
420
517
|
type: 'array',
|
|
421
518
|
items: {
|
|
422
519
|
type: 'string',
|
|
423
|
-
enum: ['base', 'playerStats', 'gamesOrMaps', 'timeline', 'liveState', 'media', 'advanced'],
|
|
520
|
+
enum: ['base', 'playerStats', 'gamesOrMaps', 'timeline', 'liveState', 'media', 'advanced', 'odds'],
|
|
424
521
|
},
|
|
425
|
-
description: 'Explicit section list; defaults to base+playerStats+gamesOrMaps+media.',
|
|
522
|
+
description: 'Explicit section list; defaults to base+playerStats+gamesOrMaps+media. "odds" (UFC) is opt-in: moneyline summarised per fighter with bookmaker count, best and median American price and implied probability, plus a count of every other market. Closing lines for a finished fight come back with currentlyOffered=false rather than being omitted.',
|
|
426
523
|
},
|
|
427
524
|
includeTimeline: boolSchema('Include timeline section (heavy).', false),
|
|
428
525
|
includeLiveState: boolSchema('Include live state/snapshots.', false),
|
|
@@ -476,6 +573,7 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
476
573
|
base: null,
|
|
477
574
|
playerStats: null,
|
|
478
575
|
gamesOrMaps: null,
|
|
576
|
+
odds: null,
|
|
479
577
|
timeline: null,
|
|
480
578
|
liveState: null,
|
|
481
579
|
media: null,
|
|
@@ -524,6 +622,28 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
524
622
|
}
|
|
525
623
|
})());
|
|
526
624
|
};
|
|
625
|
+
// Same as load(), but folds the payload through a shaper first. Raw odds are
|
|
626
|
+
// far too big to hand back whole: one bout carries up to ~950 outcomes across
|
|
627
|
+
// every bookmaker and prop market, which would bury the answer it was fetched
|
|
628
|
+
// to give.
|
|
629
|
+
const loadWith = (section, path, shape, query) => {
|
|
630
|
+
tasks.push((async () => {
|
|
631
|
+
const res = await getSection(ctx, path, query);
|
|
632
|
+
upstreamCalls += 1;
|
|
633
|
+
rateLimit = { ...rateLimit, ...res.headers };
|
|
634
|
+
if (!res.ok) {
|
|
635
|
+
partial.push(partialFromRejection(section, {
|
|
636
|
+
code: mapHttpToCode(res.status),
|
|
637
|
+
message: `${section} HTTP ${res.status}`,
|
|
638
|
+
httpStatus: res.status,
|
|
639
|
+
}));
|
|
640
|
+
sections[section] = null;
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
sections[section] = shape(res.data);
|
|
644
|
+
}
|
|
645
|
+
})());
|
|
646
|
+
};
|
|
527
647
|
if (wanted.has('playerStats')) {
|
|
528
648
|
if (game === 'lol')
|
|
529
649
|
load('playerStats', `/lol/matches/${encodeURIComponent(matchId)}/player-stats`);
|
|
@@ -577,6 +697,17 @@ Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "in
|
|
|
577
697
|
}));
|
|
578
698
|
}
|
|
579
699
|
}
|
|
700
|
+
if (wanted.has('odds')) {
|
|
701
|
+
if (game === 'ufc') {
|
|
702
|
+
loadWith('odds', `/ufc/bouts/${encodeURIComponent(matchId)}/odds`, summarizeUfcOdds);
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
partial.push(partialFromRejection('odds', {
|
|
706
|
+
code: 'NOT_IMPLEMENTED',
|
|
707
|
+
message: `Odds not curated for ${game}; UFC only today`,
|
|
708
|
+
}));
|
|
709
|
+
}
|
|
710
|
+
}
|
|
580
711
|
if (wanted.has('media')) {
|
|
581
712
|
if (game === 'cs2')
|
|
582
713
|
load('media', `/cs2/matches/${encodeURIComponent(matchId)}/demos`);
|
package/package.json
CHANGED