cito-mcp 0.4.0 → 0.4.2

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.
@@ -69,6 +69,9 @@ Do not use when: the draw bracket or a specific edition → event_card with the
69
69
 
70
70
  Tennis-only. Levels accept names (Grand Slam, WTA 1000, ATP 500, Challenger,
71
71
  ITF World Tennis Tour, Davis Cup, ...) or raw tier codes (G, M, A, C, D).
72
+ A level name that starts with ATP or WTA also fixes the tour (and conflicts with
73
+ a contradicting tour= argument are rejected), so "WTA 1000" cannot return ATP
74
+ Masters events even though the upstream filter is not tour-aware.
72
75
  Note the source does not separate ATP 500 from ATP 250 — both are ATP Tour.
73
76
 
74
77
  Parallel-safe: yes. Upstream cost: 1.`,
@@ -171,6 +174,44 @@ Parallel-safe: yes. Upstream cost: 1.`,
171
174
  });
172
175
  }
173
176
  }
177
+ /**
178
+ * A level name that starts with a tour token constrains the tour, and the
179
+ * upstream filter does not know that.
180
+ *
181
+ * The API's reverse level map is not tour-aware: `_TIER_WTA` carries BOTH
182
+ * "PM" and "M" as "WTA 1000" (M is the fallback label for a WTA row stored
183
+ * with the ATP code), so `?level=WTA 1000` resolves to `tier_code IN
184
+ * ('PM','M')` and returns every ATP Masters 1000 event alongside the WTA
185
+ * ones. Verified live: asking for "WTA 1000" returned Rome Masters, Madrid
186
+ * Masters, Monte Carlo, Miami Masters and Indian Wells Masters (all ATP).
187
+ * The same widening hits "WTA 125" (code C, shared with ATP Challengers) and
188
+ * "ATP 500"/"ATP 250" (code A, shared with WTA tour-level rows).
189
+ *
190
+ * The tool therefore asserts the invariant the label states: it sends the
191
+ * tour, and it verifies the rows that come back. Rows that contradict the
192
+ * requested level are dropped and counted rather than passed through as
193
+ * "close enough" — a filter that returns other-tour events is worse than a
194
+ * filter that returns nothing, because the caller has no way to notice.
195
+ */
196
+ const levelTourPrefix = (() => {
197
+ const m = /^(ATP|WTA)\b/i.exec(levelRaw);
198
+ return m ? m[1].toUpperCase() : null;
199
+ })();
200
+ if (levelTourPrefix && tourRaw && tourRaw !== levelTourPrefix) {
201
+ return errorEnvelope({
202
+ code: 'VALIDATION',
203
+ message: `tour ('${tourRaw}') contradicts level ('${levelRaw}'), which names the ${levelTourPrefix} tour`,
204
+ game,
205
+ source: 'tournaments',
206
+ requestId,
207
+ tookMs: Date.now() - started,
208
+ recover: [
209
+ `Drop tour, or set tour=${levelTourPrefix}`,
210
+ 'A level name that starts with ATP/WTA already determines the tour',
211
+ ],
212
+ });
213
+ }
214
+ const effectiveTour = tourRaw || levelTourPrefix || '';
174
215
  const limit = clampInt(args.limit, 20, 1, 50);
175
216
  const page = clampInt(args.page, 1, 1, 10000);
176
217
  const countryCode = typeof args.countryCode === 'string' && args.countryCode.trim()
@@ -182,7 +223,7 @@ Parallel-safe: yes. Upstream cost: 1.`,
182
223
  limit,
183
224
  page,
184
225
  ...(year !== null ? { year } : {}),
185
- ...(tourRaw ? { tour: tourRaw } : {}),
226
+ ...(effectiveTour ? { tour: effectiveTour } : {}),
186
227
  ...(levelRaw ? { level: levelRaw } : {}),
187
228
  ...(surface ? { surface } : {}),
188
229
  ...(countryCode ? { country_code: countryCode } : {}),
@@ -204,17 +245,35 @@ Parallel-safe: yes. Upstream cost: 1.`,
204
245
  }
205
246
  const root = asRecord(res.data) ?? {};
206
247
  const body = asRecord(root.data) ?? root;
207
- const items = (Array.isArray(body.items) ? body.items : []).map(normalizeTournament);
248
+ const rawItems = (Array.isArray(body.items) ? body.items : []).map(normalizeTournament);
249
+ // Verify the invariant the level label states. See levelTourPrefix above.
250
+ const contradictions = levelTourPrefix
251
+ ? rawItems.filter((t) => t.tour && t.tour.toUpperCase() !== levelTourPrefix)
252
+ : [];
253
+ const items = contradictions.length
254
+ ? rawItems.filter((t) => !t.tour || t.tour.toUpperCase() === levelTourPrefix)
255
+ : rawItems;
208
256
  const total = typeof body.total === 'number' ? body.total : items.length;
209
257
  const pageSize = typeof body.page_size === 'number' ? body.page_size : limit;
210
258
  const filters = [
211
259
  year !== null ? String(year) : null,
212
- tourRaw || null,
260
+ effectiveTour || null,
213
261
  levelRaw || null,
214
262
  surface || null,
215
263
  countryCode || null,
216
264
  q ? `"${q}"` : null,
217
265
  ].filter(Boolean);
266
+ const warnings = [];
267
+ if (contradictions.length) {
268
+ const sample = contradictions
269
+ .slice(0, 5)
270
+ .map((t) => `${t.id} (${t.tour})`)
271
+ .join(', ');
272
+ warnings.push(`The upstream level filter returned ${contradictions.length} tournament(s) that contradict level="${levelRaw}": ` +
273
+ `${sample}. They were dropped. The API's level reverse-map is not tour-aware (WTA 1000 resolves to tier codes ` +
274
+ `PM and M), which is a server-side defect; this tool sends tour=${levelTourPrefix} as well, so a repeat of this ` +
275
+ `warning means the upstream filter is widening again.`);
276
+ }
218
277
  return successEnvelope({
219
278
  pagination: {
220
279
  limit: pageSize,
@@ -230,18 +289,25 @@ Parallel-safe: yes. Upstream cost: 1.`,
230
289
  tookMs: Date.now() - started,
231
290
  upstreamCalls: 1,
232
291
  rateLimit: res.headers,
292
+ warnings: warnings.length ? warnings : undefined,
233
293
  data: {
234
294
  title: filters.length > 0
235
295
  ? `Tennis tournaments — ${filters.join(' ')}`
236
296
  : 'Tennis tournaments',
237
297
  filters: {
238
298
  year,
239
- tour: tourRaw || null,
299
+ tour: effectiveTour || null,
240
300
  level: levelRaw || null,
241
301
  surface: surface ?? null,
242
302
  countryCode: countryCode || null,
243
303
  q: q || null,
244
304
  },
305
+ levelFilterIntegrity: {
306
+ levelNamesTour: levelTourPrefix,
307
+ rowsChecked: rawItems.length,
308
+ contradictionsDropped: contradictions.length,
309
+ ok: contradictions.length === 0,
310
+ },
245
311
  items,
246
312
  total,
247
313
  page,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cito-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Standalone MCP server for the Cito esports and sports API — 42 curated outcome tools for agents (live scoreboards, round economy, opening duels, clutches, vetoes, rosters, tennis, mma).",
5
5
  "type": "module",
6
6
  "bin": {