cito-mcp 0.6.0 → 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.
@@ -376,20 +376,20 @@ async function searchGame(ctx, game, q, type, limit) {
376
376
  }
377
377
  export const resolveEntity = {
378
378
  name: 'resolve_entity',
379
- description: `Natural-language / fuzzy query → best typed entity ID(s) + game (player, team, event, tournament, match, fighter).
380
-
381
- When to use:
382
- - User named an entity without an ID ("T1", "s1mple", "IEM Cologne", "Islam Makhachev")
383
- - Need a canonical id/slug before profile or match tools
384
-
385
- Prefer over search_entities when you want one best match (or small ranked set) to chain.
386
- Prefer search_entities when browsing many results with pagination.
387
-
388
- Do not use when: you already have a stable id/slug from a prior tool.
389
-
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
-
392
- Parallel-safe: yes. API calls: 1–5.
379
+ description: `Natural-language / fuzzy query → best typed entity ID(s) + game (player, team, event, tournament, match, fighter).
380
+
381
+ When to use:
382
+ - User named an entity without an ID ("T1", "s1mple", "IEM Cologne", "Islam Makhachev")
383
+ - Need a canonical id/slug before profile or match tools
384
+
385
+ Prefer over search_entities when you want one best match (or small ranked set) to chain.
386
+ Prefer search_entities when browsing many results with pagination.
387
+
388
+ Do not use when: you already have a stable id/slug from a prior tool.
389
+
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
+
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',
@@ -441,11 +441,11 @@ Example: { "q": "T1", "game": "lol", "type": "team", "limit": 5 }`,
441
441
  ? [gameParse.game]
442
442
  : [...PRIMARY_GAMES];
443
443
  const partial = [];
444
- let upstreamCalls = 0;
444
+ let apiCalls = 0;
445
445
  const all = [];
446
446
  const results = await Promise.all(games.map((g) => searchGame(ctx, g, q, type, limit)));
447
447
  for (const r of results) {
448
- upstreamCalls += r.calls;
448
+ apiCalls += r.calls;
449
449
  all.push(...r.candidates);
450
450
  if (r.error)
451
451
  partial.push(r.error);
@@ -469,7 +469,7 @@ Example: { "q": "T1", "game": "lol", "type": "team", "limit": 5 }`,
469
469
  game: best?.game ?? gameParse.game ?? null,
470
470
  requestId,
471
471
  tookMs: Date.now() - started,
472
- upstreamCalls,
472
+ apiCalls,
473
473
  partial: partial.length ? partial : undefined,
474
474
  entities: best
475
475
  ? {
@@ -492,22 +492,22 @@ Example: { "q": "T1", "game": "lol", "type": "team", "limit": 5 }`,
492
492
  };
493
493
  export const searchEntities = {
494
494
  name: 'search_entities',
495
- description: `Browse/search teams, players, tournaments, events, fighters with type filter and pagination.
496
-
497
- When to use:
498
- - Typeahead / pickers
499
- - "List teams matching…"
500
- - Exploring entities without committing to one ID
501
- - UFC fighter lookup by name/nickname (uses /ufc/search + client re-rank)
502
-
503
- Prefer over resolve_entity when the user wants a list.
504
- Prefer resolve_entity when chaining one name into a profile tool.
505
-
506
- Do not use when: fetching a known entity profile — use team_profile or player_profile.
507
-
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
-
510
- Parallel-safe: yes. API calls: 1–3.
495
+ description: `Browse/search teams, players, tournaments, events, fighters with type filter and pagination.
496
+
497
+ When to use:
498
+ - Typeahead / pickers
499
+ - "List teams matching…"
500
+ - Exploring entities without committing to one ID
501
+ - UFC fighter lookup by name/nickname (uses /ufc/search + client re-rank)
502
+
503
+ Prefer over resolve_entity when the user wants a list.
504
+ Prefer resolve_entity when chaining one name into a profile tool.
505
+
506
+ Do not use when: fetching a known entity profile — use team_profile or player_profile.
507
+
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
+
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',
@@ -549,10 +549,10 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
549
549
  const offset = typeof cursor?.offset === 'number' ? cursor.offset : 0;
550
550
  const activeOnly = args.activeOnly !== false;
551
551
  let items = [];
552
- let upstreamCalls = 0;
552
+ let apiCalls = 0;
553
553
  let total = null;
554
- let upstreamPaged = true;
555
- let preserveUpstreamOrder = false;
554
+ let apiPaged = true;
555
+ let preserveApiOrder = false;
556
556
  const pageQuery = (extra = {}) => ({
557
557
  limit,
558
558
  offset,
@@ -563,7 +563,7 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
563
563
  });
564
564
  async function listPath(path, entityType, extra = {}) {
565
565
  const res = await fetchJson(ctx, path, { query: pageQuery(extra) });
566
- upstreamCalls += 1;
566
+ apiCalls += 1;
567
567
  if (!res.ok) {
568
568
  throw Object.assign(new Error(`HTTP ${res.status}`), {
569
569
  status: res.status,
@@ -592,7 +592,7 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
592
592
  else if (game === 'cs2') {
593
593
  if (q && (type === 'any' || !['team', 'player', 'event'].includes(type))) {
594
594
  const res = await fetchJson(ctx, '/cs2/search', { query: { q, limit } });
595
- upstreamCalls += 1;
595
+ apiCalls += 1;
596
596
  if (res.ok) {
597
597
  const envelope = asRecord(res.data) ?? {};
598
598
  const data = asRecord(envelope.data) ?? envelope;
@@ -625,7 +625,7 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
625
625
  else if (game === 'dota2') {
626
626
  if (q) {
627
627
  const res = await fetchJson(ctx, '/dota2/search', { query: { q, limit } });
628
- upstreamCalls += 1;
628
+ apiCalls += 1;
629
629
  if (res.ok) {
630
630
  const data = asRecord(res.data) ?? {};
631
631
  if (type === 'team' || type === 'any')
@@ -648,18 +648,18 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
648
648
  else if (game === 'tennis') {
649
649
  if (type === 'player' || type === 'any') {
650
650
  const res = await fetchJson(ctx, '/tennis/players/search', { query: { q, limit } });
651
- upstreamCalls += 1;
651
+ apiCalls += 1;
652
652
  if (res.ok) {
653
653
  const envelope = asRecord(res.data) ?? {};
654
654
  const data = asRecord(envelope.data) ?? envelope;
655
655
  items.push(...extractRows(data.items ?? data).map((r) => entityRef(r, 'player', game)));
656
656
  if (q)
657
- preserveUpstreamOrder = true;
657
+ preserveApiOrder = true;
658
658
  }
659
659
  }
660
660
  if (type === 'tournament' || type === 'event' || type === 'any') {
661
661
  const res = await fetchJson(ctx, '/tennis/competitions', { query: { q, page_size: limit } });
662
- upstreamCalls += 1;
662
+ apiCalls += 1;
663
663
  if (res.ok) {
664
664
  const envelope = asRecord(res.data) ?? {};
665
665
  const data = asRecord(envelope.data) ?? envelope;
@@ -669,11 +669,11 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
669
669
  }
670
670
  else if (game === 'cod') {
671
671
  if (q) {
672
- upstreamPaged = false;
672
+ apiPaged = false;
673
673
  const res = await fetchJson(ctx, '/cod/search', {
674
674
  query: { q, limit, ...(type !== 'any' ? { type: type === 'team' ? 'org' : type } : {}) },
675
675
  });
676
- upstreamCalls += 1;
676
+ apiCalls += 1;
677
677
  if (res.ok)
678
678
  items.push(...extractRows(res.data).map((r) => entityRef(r, type === 'any' ? 'unknown' : type, game)));
679
679
  }
@@ -688,9 +688,9 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
688
688
  }
689
689
  else if (game === 'ufc') {
690
690
  if (q) {
691
- upstreamPaged = false;
691
+ apiPaged = false;
692
692
  const res = await fetchJson(ctx, '/ufc/search', { query: { q } });
693
- upstreamCalls += 1;
693
+ apiCalls += 1;
694
694
  if (!res.ok) {
695
695
  throw Object.assign(new Error(`HTTP ${res.status}`), {
696
696
  status: res.status,
@@ -761,7 +761,7 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
761
761
  source: 'search_entities',
762
762
  requestId,
763
763
  tookMs: Date.now() - started,
764
- upstreamCalls,
764
+ apiCalls,
765
765
  httpStatus: e.status,
766
766
  rateLimit: e.headers,
767
767
  });
@@ -777,7 +777,7 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
777
777
  const scoreOf = (it) => typeof it.meta?.score === 'number'
778
778
  ? it.meta.score
779
779
  : rankScore(q, it.name, it.id, it.slug, it.meta?.nickname);
780
- const upstreamTotal = total;
780
+ const unfilteredTotal = total;
781
781
  let droppedBelowFloor = [];
782
782
  const ENTITY_TYPES = new Set(['player', 'team', 'fighter']);
783
783
  const protectedRow = q ? items.find((it) => ENTITY_TYPES.has(it.type)) ?? null : null;
@@ -803,15 +803,15 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
803
803
  if (droppedBelowFloor.length)
804
804
  items = items.filter(keep);
805
805
  }
806
- if (q && !preserveUpstreamOrder) {
806
+ if (q && !preserveApiOrder) {
807
807
  items.sort((a, b) => {
808
808
  const am = a.meta?.matchedBy === 'top-hit' ? 1 : 0;
809
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
  }
813
- const pageItems = upstreamPaged ? items.slice(0, limit) : items.slice(offset, offset + limit);
814
- const hasMore = upstreamPaged
813
+ const pageItems = apiPaged ? items.slice(0, limit) : items.slice(offset, offset + limit);
814
+ const hasMore = apiPaged
815
815
  ? (total != null ? offset + pageItems.length < total : pageItems.length >= limit)
816
816
  : offset + pageItems.length < items.length;
817
817
  const floorWarnings = [];
@@ -829,7 +829,7 @@ Example: { "game": "ufc", "q": "Jon Jones", "type": "fighter", "limit": 10 }`,
829
829
  game,
830
830
  requestId,
831
831
  tookMs: Date.now() - started,
832
- upstreamCalls,
832
+ apiCalls,
833
833
  warnings: floorWarnings.length ? floorWarnings : undefined,
834
834
  pagination: {
835
835
  limit,
@@ -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
- unfilteredTotal: upstreamTotal ?? null,
852
+ unfilteredTotal: unfilteredTotal ?? null,
853
853
  },
854
854
  }
855
855
  : {}),
@@ -134,13 +134,13 @@ Parallel-safe: yes. API calls: 1 or 2.`,
134
134
  const limit = clampInt(args.limit, 20, 1, 50);
135
135
  const wantScheduled = includeRaw === 'both' || includeRaw === 'scheduled';
136
136
  const wantCompleted = includeRaw === 'both' || includeRaw === 'completed';
137
- const upstreamPage = 500;
137
+ const apiPage = 500;
138
138
  const settled = await Promise.allSettled([
139
139
  wantScheduled
140
- ? fetchJson(ctx, '/tennis/schedule', { query: { date, limit: upstreamPage } })
140
+ ? fetchJson(ctx, '/tennis/schedule', { query: { date, limit: apiPage } })
141
141
  : Promise.resolve(null),
142
142
  wantCompleted
143
- ? fetchJson(ctx, '/tennis/matches/completed', { query: { date, limit: upstreamPage } })
143
+ ? fetchJson(ctx, '/tennis/matches/completed', { query: { date, limit: apiPage } })
144
144
  : Promise.resolve(null),
145
145
  ]);
146
146
  const rejected = [];
@@ -208,7 +208,7 @@ Parallel-safe: yes. API calls: 1 or 2.`,
208
208
  source: 'tennis_schedule',
209
209
  requestId,
210
210
  tookMs: Date.now() - started,
211
- upstreamCalls: rejected.length,
211
+ apiCalls: rejected.length,
212
212
  recover: ['Check the date is a real calendar day', 'Retry, or widen include to both'],
213
213
  });
214
214
  }
@@ -255,14 +255,14 @@ Parallel-safe: yes. API calls: 1 or 2.`,
255
255
  }
256
256
  : {}),
257
257
  };
258
- const upstreamCalls = (wantScheduled ? 1 : 0) + (wantCompleted ? 1 : 0);
258
+ const apiCalls = (wantScheduled ? 1 : 0) + (wantCompleted ? 1 : 0);
259
259
  if (rejected.length > 0) {
260
260
  return successEnvelope({
261
261
  source: 'tennis_schedule',
262
262
  game,
263
263
  requestId,
264
264
  tookMs: Date.now() - started,
265
- upstreamCalls,
265
+ apiCalls,
266
266
  data,
267
267
  partial: rejected.map((r) => partialFromRejection(r.section, {
268
268
  code: r.code,
@@ -270,7 +270,7 @@ Parallel-safe: yes. API calls: 1 or 2.`,
270
270
  ...(r.httpStatus !== undefined ? { httpStatus: r.httpStatus } : {}),
271
271
  })),
272
272
  warnings: [
273
- `${rejected.length} of ${upstreamCalls} schedule section(s) failed; the rest of the day is still returned.`,
273
+ `${rejected.length} of ${apiCalls} schedule section(s) failed; the rest of the day is still returned.`,
274
274
  ],
275
275
  });
276
276
  }
@@ -279,7 +279,7 @@ Parallel-safe: yes. API calls: 1 or 2.`,
279
279
  game,
280
280
  requestId,
281
281
  tookMs: Date.now() - started,
282
- upstreamCalls,
282
+ apiCalls,
283
283
  data,
284
284
  });
285
285
  },
@@ -271,7 +271,7 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
271
271
  source: 'standings',
272
272
  requestId,
273
273
  tookMs: Date.now() - started,
274
- upstreamCalls: 1,
274
+ apiCalls: 1,
275
275
  httpStatus: res.status,
276
276
  rateLimit: res.headers,
277
277
  });
@@ -293,7 +293,7 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
293
293
  source: 'standings',
294
294
  requestId,
295
295
  tookMs: Date.now() - started,
296
- upstreamCalls: 1,
296
+ apiCalls: 1,
297
297
  recover: [
298
298
  'Use team_profile with radar for style/form',
299
299
  'Use call_api if a standings path is added later',
@@ -305,7 +305,7 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
305
305
  game,
306
306
  requestId,
307
307
  tookMs: Date.now() - started,
308
- upstreamCalls: 1,
308
+ apiCalls: 1,
309
309
  rateLimit: res.headers,
310
310
  warnings: ['Dota standings composed from team worldRanking fields; sparse'],
311
311
  data: {
@@ -327,7 +327,7 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
327
327
  source: 'standings',
328
328
  requestId,
329
329
  tookMs: Date.now() - started,
330
- upstreamCalls: 1,
330
+ apiCalls: 1,
331
331
  httpStatus: res.status,
332
332
  rateLimit: res.headers,
333
333
  });
@@ -361,13 +361,13 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
361
361
  });
362
362
  }
363
363
  const obj = asRecord(res.data);
364
- const upstreamMeta = asRecord(obj?.meta) ?? {};
365
- const upstreamTotal = typeof obj?.total === 'number' ? obj.total : null;
364
+ const apiMeta = asRecord(obj?.meta) ?? {};
365
+ const unfilteredTotal = typeof obj?.total === 'number' ? obj.total : null;
366
366
  const rankedPlayers = typeof obj?.ranked_players === 'number' ? obj.ranked_players : null;
367
367
  const totalPages = typeof obj?.total_pages === 'number' ? obj.total_pages : null;
368
- const upstreamHasNext = typeof obj?.has_next === 'boolean' ? obj.has_next : null;
369
- const effectiveTotal = upstreamTotal ?? allRows.length;
370
- const effectiveHasMore = upstreamHasNext ?? offset + rows.length < effectiveTotal;
368
+ const apiHasNext = typeof obj?.has_next === 'boolean' ? obj.has_next : null;
369
+ const effectiveTotal = unfilteredTotal ?? allRows.length;
370
+ const effectiveHasMore = apiHasNext ?? offset + rows.length < effectiveTotal;
371
371
  const standingsPagination = {
372
372
  limit,
373
373
  offset,
@@ -376,7 +376,7 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
376
376
  nextCursor: effectiveHasMore ? String(offset + rows.length) : null,
377
377
  prevCursor: offset > 0 ? String(Math.max(0, offset - limit)) : null,
378
378
  };
379
- const dataQuality = asRecord(upstreamMeta.dataQuality) ?? {};
379
+ const dataQuality = asRecord(apiMeta.dataQuality) ?? {};
380
380
  const warnings = [];
381
381
  const pushWarning = (value) => {
382
382
  if (typeof value !== 'string')
@@ -385,23 +385,23 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
385
385
  if (trimmed && !warnings.includes(trimmed))
386
386
  warnings.push(trimmed);
387
387
  };
388
- pushWarning(upstreamMeta.warning);
388
+ pushWarning(apiMeta.warning);
389
389
  pushWarning(dataQuality.warning);
390
390
  const tennisCoverage = game === 'tennis' ? asRecord(obj?.list_coverage) : null;
391
391
  if (tennisCoverage && tennisCoverage.status === 'partial')
392
392
  pushWarning(tennisCoverage.note);
393
- if (Array.isArray(upstreamMeta.warnings)) {
394
- for (const w of upstreamMeta.warnings)
393
+ if (Array.isArray(apiMeta.warnings)) {
394
+ for (const w of apiMeta.warnings)
395
395
  pushWarning(w);
396
396
  }
397
- const dataFreshness = pickString(upstreamMeta.dataFreshness, upstreamMeta.freshnessStatus, dataQuality.dataFreshness);
398
- const status = pickString(upstreamMeta.status, dataQuality.status);
397
+ const dataFreshness = pickString(apiMeta.dataFreshness, apiMeta.freshnessStatus, dataQuality.dataFreshness);
398
+ const status = pickString(apiMeta.status, dataQuality.status);
399
399
  if (status === 'fallback') {
400
400
  pushWarning('Standings served from last-known-good rankings (fallback).');
401
401
  }
402
- if (dataFreshness === 'cached' || upstreamMeta.stale === true) {
403
- pushWarning(typeof upstreamMeta.dataAgeHours === 'number'
404
- ? `Rankings dataFreshness=cached (ageHours=${upstreamMeta.dataAgeHours}).`
402
+ if (dataFreshness === 'cached' || apiMeta.stale === true) {
403
+ pushWarning(typeof apiMeta.dataAgeHours === 'number'
404
+ ? `Rankings dataFreshness=cached (ageHours=${apiMeta.dataAgeHours}).`
405
405
  : 'Rankings dataFreshness=cached or stale.');
406
406
  }
407
407
  return successEnvelope({
@@ -410,7 +410,7 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
410
410
  game,
411
411
  requestId,
412
412
  tookMs: Date.now() - started,
413
- upstreamCalls: 1,
413
+ apiCalls: 1,
414
414
  rateLimit: res.headers,
415
415
  warnings: warnings.length ? warnings : undefined,
416
416
  data: {
@@ -432,16 +432,16 @@ Example: { "game": "cod", "season": "2026", "limit": 50 }`,
432
432
  ? {
433
433
  rankingDate: pickString(obj?.ranking_date) ?? null,
434
434
  listCoverage: tennisCoverage ? normalizeCoverage(tennisCoverage) : null,
435
- updatedAt: pickString(obj?.updatedAt, obj?.lastUpdated, upstreamMeta.syncedAt, upstreamMeta.fetchedAt, obj?.ranking_date) ?? null,
435
+ updatedAt: pickString(obj?.updatedAt, obj?.lastUpdated, apiMeta.syncedAt, apiMeta.fetchedAt, obj?.ranking_date) ?? null,
436
436
  }
437
437
  : {
438
- updatedAt: pickString(obj?.updatedAt, obj?.lastUpdated, upstreamMeta.syncedAt, upstreamMeta.fetchedAt) ??
438
+ updatedAt: pickString(obj?.updatedAt, obj?.lastUpdated, apiMeta.syncedAt, apiMeta.fetchedAt) ??
439
439
  null,
440
440
  }),
441
441
  ...(dataFreshness ? { dataFreshness } : {}),
442
442
  ...(status ? { status } : {}),
443
- ...(upstreamMeta.warning || dataQuality.warning
444
- ? { warning: pickString(upstreamMeta.warning, dataQuality.warning) }
443
+ ...(apiMeta.warning || dataQuality.warning
444
+ ? { warning: pickString(apiMeta.warning, dataQuality.warning) }
445
445
  : {}),
446
446
  },
447
447
  });