cito-mcp 0.3.17 → 0.3.19
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/insight.js +11 -3
- package/dist/tools/normalize.js +48 -0
- package/dist/tools/resolve.js +52 -35
- package/dist/tools/team.js +44 -27
- package/package.json +1 -1
package/dist/tools/insight.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { clampInt, extractRows, fetchJson, gameNotIncludedHint, asRecord, pickString, unwrapPayload, } from '../client.js';
|
|
5
5
|
import { errorEnvelope, mapHttpToCode, newRequestId, partialFromRejection, successEnvelope, } from '../envelope.js';
|
|
6
|
-
import { normalizeMatch, presentSides, sortByCardOrder } from './normalize.js';
|
|
6
|
+
import { chooseNamedEntity, normalizeMatch, presentSides, sortByCardOrder } from './normalize.js';
|
|
7
7
|
import { boolSchema, gameSchema, isPrimaryGame, limitSchema, parseGame, stringSchema, } from './types.js';
|
|
8
8
|
async function loadSide(ctx, game, side, recentLimit, includeRosters) {
|
|
9
9
|
const partial = [];
|
|
@@ -663,9 +663,17 @@ async function resolveEventKey(ctx, game, q) {
|
|
|
663
663
|
calls += 1;
|
|
664
664
|
rateLimit = res.headers;
|
|
665
665
|
const rows = extractRows(unwrapPayload(res.data) ?? res.data).map((r) => asRecord(r) ?? {});
|
|
666
|
+
// .find() returned whatever the calendar happened to list first, and the
|
|
667
|
+
// calendar lists the WTA row before the ATP one for a shared name, while
|
|
668
|
+
// /tennis/competitions (which resolve_entity reads) lists them the other
|
|
669
|
+
// way round. Same query, two tools, two ids. One shared rule now settles
|
|
670
|
+
// both; see chooseNamedEntity.
|
|
666
671
|
hit =
|
|
667
|
-
rows
|
|
668
|
-
|
|
672
|
+
chooseNamedEntity(rows, needle, (r) => ({
|
|
673
|
+
id: pickString(r.id, r.tournament_id) ?? '',
|
|
674
|
+
name: pickString(r.name) ?? '',
|
|
675
|
+
year: r.year,
|
|
676
|
+
})) ?? undefined;
|
|
669
677
|
if (hit)
|
|
670
678
|
break;
|
|
671
679
|
}
|
package/dist/tools/normalize.js
CHANGED
|
@@ -638,3 +638,51 @@ export function rankScore(query, name, id, slug, ...extra) {
|
|
|
638
638
|
}
|
|
639
639
|
return best;
|
|
640
640
|
}
|
|
641
|
+
/**
|
|
642
|
+
* Edition year encoded in a tennis-style entity id ("atp_2026_580" -> 2026).
|
|
643
|
+
*
|
|
644
|
+
* `/tennis/competitions` returns no `year` field while the calendar does, so
|
|
645
|
+
* the id is the only signal available on both, and the two tools that disagreed
|
|
646
|
+
* about "Australian Open" were each reading a different one of those endpoints.
|
|
647
|
+
*/
|
|
648
|
+
export function editionYear(id, explicit) {
|
|
649
|
+
const direct = Number(explicit);
|
|
650
|
+
if (Number.isFinite(direct) && direct > 1800)
|
|
651
|
+
return direct;
|
|
652
|
+
const match = /^[a-z]+_(\d{4})_/i.exec(String(id ?? ''));
|
|
653
|
+
return match ? Number(match[1]) : null;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Deterministic winner among rows whose names match a query equally well.
|
|
657
|
+
*
|
|
658
|
+
* "Australian Open" is two rows in every season, one ATP and one WTA, sharing a
|
|
659
|
+
* name exactly. resolve_entity read /tennis/competitions, which lists ATP
|
|
660
|
+
* first; event_card read /tennis/tournaments/calendar, which lists WTA first;
|
|
661
|
+
* and neither applied a tiebreak, so the same question got atp_2026_580 from
|
|
662
|
+
* one tool and wta_2026_580 from the other. Neither answer was wrong on its
|
|
663
|
+
* own. The absence of a rule was the bug.
|
|
664
|
+
*
|
|
665
|
+
* Order: an exact name beats a substring, a newer edition beats an older one,
|
|
666
|
+
* and the id settles whatever is left. That last step is arbitrary, and that is
|
|
667
|
+
* fine -- it only has to be FIXED, which is the property that was missing.
|
|
668
|
+
* Callers that need the alternatives still surface them; this only decides
|
|
669
|
+
* which single row is called "best".
|
|
670
|
+
*/
|
|
671
|
+
export function chooseNamedEntity(rows, needle, read) {
|
|
672
|
+
const want = needle.trim().toLowerCase();
|
|
673
|
+
if (!want || !rows.length)
|
|
674
|
+
return null;
|
|
675
|
+
const scored = rows.map((row) => ({ row, ref: read(row) }));
|
|
676
|
+
const exact = scored.filter((s) => s.ref.name.trim().toLowerCase() === want);
|
|
677
|
+
const pool = exact.length
|
|
678
|
+
? exact
|
|
679
|
+
: scored.filter((s) => s.ref.name.trim().toLowerCase().includes(want));
|
|
680
|
+
if (!pool.length)
|
|
681
|
+
return null;
|
|
682
|
+
pool.sort((a, b) => {
|
|
683
|
+
const ay = editionYear(a.ref.id, a.ref.year) ?? -1;
|
|
684
|
+
const by = editionYear(b.ref.id, b.ref.year) ?? -1;
|
|
685
|
+
return by - ay || a.ref.id.localeCompare(b.ref.id);
|
|
686
|
+
});
|
|
687
|
+
return pool[0].row;
|
|
688
|
+
}
|
package/dist/tools/resolve.js
CHANGED
|
@@ -3,8 +3,25 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { clampInt, decodeCursor, encodeCursor, extractRows, fetchJson, gameNotIncludedHint, pickString, asRecord, } from '../client.js';
|
|
5
5
|
import { DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT, errorEnvelope, mapHttpToCode, newRequestId, partialFromRejection, successEnvelope, } from '../envelope.js';
|
|
6
|
-
import { entityRef, rankScore } from './normalize.js';
|
|
6
|
+
import { editionYear, entityRef, rankScore } from './normalize.js';
|
|
7
7
|
import { boolSchema, gameSchema, isPrimaryGame, limitSchema, parseGame, PRIMARY_GAMES, stringSchema, } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Ranking order whose tiebreak never depends on the order upstream happened to
|
|
10
|
+
* return rows in.
|
|
11
|
+
*
|
|
12
|
+
* "Australian Open" is two rows with byte-identical names and identical fuzzy
|
|
13
|
+
* scores, one ATP and one WTA. The old comparator ran out of criteria there and
|
|
14
|
+
* left the winner to the source ordering, and event_card reads a different
|
|
15
|
+
* source that orders them the other way round, so the two tools answered the
|
|
16
|
+
* same question with different ids. Newest edition, then id, matching
|
|
17
|
+
* chooseNamedEntity so both paths land on the same row.
|
|
18
|
+
*/
|
|
19
|
+
function byRank(a, b) {
|
|
20
|
+
return (b.score - a.score
|
|
21
|
+
|| a.name.localeCompare(b.name)
|
|
22
|
+
|| ((editionYear(b.id) ?? -1) - (editionYear(a.id) ?? -1))
|
|
23
|
+
|| a.id.localeCompare(b.id));
|
|
24
|
+
}
|
|
8
25
|
/**
|
|
9
26
|
* Query aliases for orgs whose common name is not their slug. Kept deliberately
|
|
10
27
|
* small and one-directional: these map what a person types onto the canonical
|
|
@@ -54,7 +71,7 @@ function pushCandidates(out, game, type, rows, q, limit) {
|
|
|
54
71
|
...(Object.keys(secondary).length ? { secondaryIds: secondary } : {}),
|
|
55
72
|
});
|
|
56
73
|
}
|
|
57
|
-
out.sort(
|
|
74
|
+
out.sort(byRank);
|
|
58
75
|
if (out.length > limit * 3)
|
|
59
76
|
out.length = limit * 3;
|
|
60
77
|
}
|
|
@@ -133,7 +150,7 @@ async function ufcSearch(ctx, q, type, limit) {
|
|
|
133
150
|
// Dedupe by type+id
|
|
134
151
|
const seen = new Set();
|
|
135
152
|
const deduped = [];
|
|
136
|
-
for (const c of candidates.sort(
|
|
153
|
+
for (const c of candidates.sort(byRank)) {
|
|
137
154
|
const key = `${c.type}:${c.id}:${c.slug ?? ''}`;
|
|
138
155
|
if (seen.has(key))
|
|
139
156
|
continue;
|
|
@@ -394,25 +411,25 @@ async function searchGame(ctx, game, q, type, limit) {
|
|
|
394
411
|
}),
|
|
395
412
|
};
|
|
396
413
|
}
|
|
397
|
-
candidates.sort(
|
|
414
|
+
candidates.sort(byRank);
|
|
398
415
|
return { candidates: candidates.slice(0, limit), calls };
|
|
399
416
|
}
|
|
400
417
|
export const resolveEntity = {
|
|
401
418
|
name: 'resolve_entity',
|
|
402
|
-
description: `Natural-language / fuzzy query → best typed entity ID(s) + game (player, team, event, tournament, match, fighter).
|
|
403
|
-
|
|
404
|
-
When to use:
|
|
405
|
-
- User named an entity without an ID ("T1", "s1mple", "IEM Cologne", "Islam Makhachev")
|
|
406
|
-
- Need a canonical id/slug before profile or match tools
|
|
407
|
-
|
|
408
|
-
Prefer over search_entities when you want one best match (or small ranked set) to chain.
|
|
409
|
-
Prefer search_entities when browsing many results with pagination.
|
|
410
|
-
|
|
411
|
-
Do not use when: you already have a stable id/slug from a prior tool.
|
|
412
|
-
|
|
413
|
-
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.
|
|
414
|
-
|
|
415
|
-
Parallel-safe: yes. Upstream cost: 1–5.
|
|
419
|
+
description: `Natural-language / fuzzy query → best typed entity ID(s) + game (player, team, event, tournament, match, fighter).
|
|
420
|
+
|
|
421
|
+
When to use:
|
|
422
|
+
- User named an entity without an ID ("T1", "s1mple", "IEM Cologne", "Islam Makhachev")
|
|
423
|
+
- Need a canonical id/slug before profile or match tools
|
|
424
|
+
|
|
425
|
+
Prefer over search_entities when you want one best match (or small ranked set) to chain.
|
|
426
|
+
Prefer search_entities when browsing many results with pagination.
|
|
427
|
+
|
|
428
|
+
Do not use when: you already have a stable id/slug from a prior tool.
|
|
429
|
+
|
|
430
|
+
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.
|
|
431
|
+
|
|
432
|
+
Parallel-safe: yes. Upstream cost: 1–5.
|
|
416
433
|
Example: { "q": "T1", "game": "lol", "type": "team", "limit": 5 }`,
|
|
417
434
|
inputSchema: {
|
|
418
435
|
type: 'object',
|
|
@@ -473,7 +490,7 @@ Example: { "q": "T1", "game": "lol", "type": "team", "limit": 5 }`,
|
|
|
473
490
|
if (r.error)
|
|
474
491
|
partial.push(r.error);
|
|
475
492
|
}
|
|
476
|
-
all.sort(
|
|
493
|
+
all.sort(byRank);
|
|
477
494
|
const candidates = all.slice(0, limit);
|
|
478
495
|
const best = candidates[0] ?? null;
|
|
479
496
|
// An exact identity hit is not ambiguous, whatever else scored near it.
|
|
@@ -520,22 +537,22 @@ Example: { "q": "T1", "game": "lol", "type": "team", "limit": 5 }`,
|
|
|
520
537
|
};
|
|
521
538
|
export const searchEntities = {
|
|
522
539
|
name: 'search_entities',
|
|
523
|
-
description: `Browse/search teams, players, tournaments, events, fighters with type filter and pagination.
|
|
524
|
-
|
|
525
|
-
When to use:
|
|
526
|
-
- Typeahead / pickers
|
|
527
|
-
- "List teams matching…"
|
|
528
|
-
- Exploring entities without committing to one ID
|
|
529
|
-
- UFC fighter lookup by name/nickname (uses /ufc/search + client re-rank)
|
|
530
|
-
|
|
531
|
-
Prefer over resolve_entity when the user wants a list.
|
|
532
|
-
Prefer resolve_entity when chaining one name into a profile tool.
|
|
533
|
-
|
|
534
|
-
Do not use when: fetching a known entity profile — use team_profile or player_profile.
|
|
535
|
-
|
|
536
|
-
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.
|
|
537
|
-
|
|
538
|
-
Parallel-safe: yes. Upstream cost: 1–3.
|
|
540
|
+
description: `Browse/search teams, players, tournaments, events, fighters with type filter and pagination.
|
|
541
|
+
|
|
542
|
+
When to use:
|
|
543
|
+
- Typeahead / pickers
|
|
544
|
+
- "List teams matching…"
|
|
545
|
+
- Exploring entities without committing to one ID
|
|
546
|
+
- UFC fighter lookup by name/nickname (uses /ufc/search + client re-rank)
|
|
547
|
+
|
|
548
|
+
Prefer over resolve_entity when the user wants a list.
|
|
549
|
+
Prefer resolve_entity when chaining one name into a profile tool.
|
|
550
|
+
|
|
551
|
+
Do not use when: fetching a known entity profile — use team_profile or player_profile.
|
|
552
|
+
|
|
553
|
+
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.
|
|
554
|
+
|
|
555
|
+
Parallel-safe: yes. Upstream cost: 1–3.
|
|
539
556
|
Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
|
|
540
557
|
inputSchema: {
|
|
541
558
|
type: 'object',
|
package/dist/tools/team.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* team_profile + head_to_head
|
|
3
3
|
*/
|
|
4
|
-
import { clampInt, extractRows, fetchJson, gameNotIncludedHint, asRecord, pickString, } from '../client.js';
|
|
4
|
+
import { clampInt, extractRows, fetchJson, gameNotIncludedHint, asRecord, pickString, unwrapPayload, } from '../client.js';
|
|
5
5
|
import { errorEnvelope, mapHttpToCode, newRequestId, partialFromRejection, successEnvelope, } from '../envelope.js';
|
|
6
6
|
import { normalizeMatch } from './normalize.js';
|
|
7
7
|
import { boolSchema, gameSchema, isPrimaryGame, limitSchema, parseGame, PRIMARY_GAMES, stringSchema, } from './types.js';
|
|
@@ -24,22 +24,36 @@ function teamIdentity(game, raw, idHint, slugHint) {
|
|
|
24
24
|
name,
|
|
25
25
|
game,
|
|
26
26
|
region: pickString(nested.region, nested.country) ?? null,
|
|
27
|
+
// CS2 publishes countryName/countryCode and worldRanking on the team
|
|
28
|
+
// resource and none of it reached this card: a team page rendered with no
|
|
29
|
+
// flag and no ladder position while the API had both. Spirit came back as
|
|
30
|
+
// {id, slug, name, game, region} with world #1 nowhere in sight.
|
|
31
|
+
country: pickString(nested.countryName, nested.country, nested.countryCode) ?? null,
|
|
32
|
+
countryCode: pickString(nested.countryCode) ?? null,
|
|
33
|
+
worldRanking: numberOrNull(nested.worldRanking ?? nested.ranking ?? nested.rank),
|
|
27
34
|
};
|
|
28
35
|
}
|
|
36
|
+
/** A finite number, or null. Ranks arrive as number or numeric string. */
|
|
37
|
+
function numberOrNull(value) {
|
|
38
|
+
if (value === null || value === undefined || value === '')
|
|
39
|
+
return null;
|
|
40
|
+
const parsed = Number(value);
|
|
41
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
42
|
+
}
|
|
29
43
|
export const teamProfile = {
|
|
30
44
|
name: 'team_profile',
|
|
31
|
-
description: `Team/org card: identity, roster, recent matches, and form/trends/radar when available.
|
|
32
|
-
|
|
33
|
-
When to use:
|
|
34
|
-
- Team page / "who is on this roster?"
|
|
35
|
-
- Builder team screen sample
|
|
36
|
-
|
|
37
|
-
Prefer over: separate roster + matches + detail via call_api.
|
|
38
|
-
|
|
39
|
-
Do not use when: UFC fighters → player_profile; unknown name → resolve_entity first.
|
|
40
|
-
Dota may return partial roster (API gap). Prefer slug for lol/cod; teamId for cs2.
|
|
41
|
-
|
|
42
|
-
Parallel-safe: yes. Upstream cost: 2–4.
|
|
45
|
+
description: `Team/org card: identity, roster, recent matches, and form/trends/radar when available.
|
|
46
|
+
|
|
47
|
+
When to use:
|
|
48
|
+
- Team page / "who is on this roster?"
|
|
49
|
+
- Builder team screen sample
|
|
50
|
+
|
|
51
|
+
Prefer over: separate roster + matches + detail via call_api.
|
|
52
|
+
|
|
53
|
+
Do not use when: UFC fighters → player_profile; unknown name → resolve_entity first.
|
|
54
|
+
Dota may return partial roster (API gap). Prefer slug for lol/cod; teamId for cs2.
|
|
55
|
+
|
|
56
|
+
Parallel-safe: yes. Upstream cost: 2–4.
|
|
43
57
|
Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
|
|
44
58
|
inputSchema: {
|
|
45
59
|
type: 'object',
|
|
@@ -374,8 +388,11 @@ Example: { "game": "lol", "slug": "t1", "recentLimit": 10 }`,
|
|
|
374
388
|
const res = await fetchJson(ctx, `/cs2/teams/${encodeURIComponent(tid)}/trends`);
|
|
375
389
|
upstreamCalls += 1;
|
|
376
390
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
391
|
+
// res.data is the whole upstream body, so this nested
|
|
392
|
+
// {success, data:{...}} inside our own envelope. Every other field
|
|
393
|
+
// here is normalized; this one leaked the REST wrapper to the agent.
|
|
377
394
|
if (res.ok)
|
|
378
|
-
form = res.data;
|
|
395
|
+
form = unwrapPayload(res.data) ?? res.data;
|
|
379
396
|
else {
|
|
380
397
|
partial.push(partialFromRejection('form', {
|
|
381
398
|
code: mapHttpToCode(res.status),
|
|
@@ -545,19 +562,19 @@ function winnerSide(match, a) {
|
|
|
545
562
|
}
|
|
546
563
|
export const headToHead = {
|
|
547
564
|
name: 'head_to_head',
|
|
548
|
-
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.
|
|
549
|
-
|
|
550
|
-
When to use:
|
|
551
|
-
- Rivalry / series record questions
|
|
552
|
-
- Supporting context for previews
|
|
553
|
-
|
|
554
|
-
Prefer over: agent-side double match-list filtering.
|
|
555
|
-
|
|
556
|
-
Do not use when: single-side form only → team_profile or player_profile.
|
|
557
|
-
|
|
558
|
-
Caveat: Dota filters are weaker; expect meta.warnings when data is sparse.
|
|
559
|
-
|
|
560
|
-
Parallel-safe: yes. Upstream cost: 2–4.
|
|
565
|
+
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.
|
|
566
|
+
|
|
567
|
+
When to use:
|
|
568
|
+
- Rivalry / series record questions
|
|
569
|
+
- Supporting context for previews
|
|
570
|
+
|
|
571
|
+
Prefer over: agent-side double match-list filtering.
|
|
572
|
+
|
|
573
|
+
Do not use when: single-side form only → team_profile or player_profile.
|
|
574
|
+
|
|
575
|
+
Caveat: Dota filters are weaker; expect meta.warnings when data is sparse.
|
|
576
|
+
|
|
577
|
+
Parallel-safe: yes. Upstream cost: 2–4.
|
|
561
578
|
Example: { "game": "cs2", "sideA": "faze", "sideB": "navi", "limit": 20 }`,
|
|
562
579
|
inputSchema: {
|
|
563
580
|
type: 'object',
|
package/package.json
CHANGED