cito-mcp 0.4.4 → 0.4.5

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 CHANGED
@@ -195,15 +195,15 @@ on `event_card` bout corners and on `player_profile`. No extra call, no N+1.
195
195
  "record": { "wins": 28, "losses": 1, "draws": 0, "text": "28-1-0 (W-L-D)" },
196
196
  "championStatus": "champion",
197
197
  "images": {
198
- "headshotUrl": "https://ufc.com/…/MAKHACHEV_ISLAM_BELT_01-18.png",
199
- "bodyImageUrl": "https://ufc.com/…/athlete_bio_full_body/…",
200
- "imageUrl": "https://ufc.com/…/event_fight_card_upper_body/…",
198
+ "headshotUrl": "https://…/MAKHACHEV_ISLAM_BELT_01-18.png",
199
+ "bodyImageUrl": "https://…/athlete_bio_full_body/…",
200
+ "imageUrl": "https://…/event_fight_card_upper_body/…",
201
201
  "proxiedImageUrl": "https://api.citoapi.com/api/v1/public/images/ufc/aHR0cHM6…"
202
202
  }
203
203
  }
204
204
  ```
205
205
 
206
- **Use `proxiedImageUrl` in a browser.** `ufc.com` sends no CORS header and can
206
+ **Use `proxiedImageUrl` in a browser.** the image host sends no CORS header and can
207
207
  hotlink-block, so raw URLs render as broken images in a web UI. The proxied URL
208
208
  is served by the API and is safe to put in an `<img src>`.
209
209
 
@@ -213,7 +213,7 @@ never partially shaped — if any image exists, all four keys are present.
213
213
 
214
214
  Tennis is the exception on purpose: the feed supplies a single `portrait_url`
215
215
  (Wikimedia Commons) and no square crop, so that one URL fills both `headshotUrl`
216
- and `imageUrl`, and `proxiedImageUrl` stays `null` because only `ufc.com` URLs have
216
+ and `imageUrl`, and `proxiedImageUrl` stays `null` because only fighter image URLs have
217
217
  a proxy.
218
218
 
219
219
  **Tennis images carry their licence and credit** — and a UI is expected to render
@@ -560,7 +560,7 @@ Second tennis correctness pass, from the 2026-09-12 re-sweep. Five defects, each
560
560
  Tennis correctness pass, driven by the 2026-09-12 sweep (`reports/tennis-mcp-sweep-2026-09-12.md`). Every item was reproduced against live ids and re-verified after the fix.
561
561
 
562
562
  - **`match_summary` tennis scoreline was always `- : -`.** `/tennis/matches/{id}` puts no `sets_won` on its player objects, so the sets-won score was null for every completed match while the same payload held the real score in `score` and `sets[]`. Sets won are now derived (explicit → `winner_sets_won` → `sets[]` → `score` string), `score.detail` carries the game score, and a set's `completed` is inferred when the archive omits `is_completed`. The winner-oriented `winner_games`/`loser_games` pair is deliberately not used as a side score. `bestOf` now reads `best_of`.
563
- - **`match_details` denied tennis odds existed.** The `odds` section was gated to UFC; the same match returned FanDuel/Matchbook prices from `tennis_odds`. Both surfaces now share one projection (`summarizeTennisOdds`), and `playerStats` (previously always `null` for tennis) is filled from `/tennis/matches/{id}/stats`.
563
+ - **`match_details` denied tennis odds existed.** The `odds` section was gated to UFC; the same match returned bookmaker prices from `tennis_odds`. Both surfaces now share one projection (`summarizeTennisOdds`), and `playerStats` (previously always `null` for tennis) is filled from `/tennis/matches/{id}/stats`.
564
564
  - **`player_stats.bySurface` was always four nulls.** The endpoint publishes one object per surface and the projector coerced each to a number. Real per-surface blocks, `servingStats`, and an explicit `unavailable` map for sections the endpoint does not publish.
565
565
  - **`limit` was ignored** by `player_rankings_history` (asked 4, got 342) and by `/tennis/matches/completed`. Both are bounded locally now, with `*Returned` versus `*Count` so the day total is never confused with the page.
566
566
  - **`tournaments level="WTA 1000"` returned ATP Masters events.** The API's level reverse-map is not tour-aware (`_TIER_WTA` maps both `PM` and `M` to "WTA 1000"), so the tool now sends and enforces the tour the level name states, and drops + counts contradicting rows. Fixed at the source too — the same widening also leaked `WTA 125`, `Challenger`, `Davis Cup` and `Billie Jean King Cup`.
package/dist/client.js CHANGED
@@ -1,25 +1,9 @@
1
- /**
2
- * Authenticated Cito REST client for curated tools.
3
- * Auth: CITO_API_KEY → x-api-key. Logs never include the key.
4
- */
5
1
  import { PACKAGE_VERSION } from './version.js';
6
2
  export const DEFAULT_API_BASE = 'https://api.citoapi.com/api/v1';
7
3
  export const DEFAULT_MAX_RESPONSE_BYTES = 100 * 1024;
8
- /** stderr ONLY — stdout is the MCP stdio channel. */
9
4
  export function log(message) {
10
5
  console.error(`[cito-mcp] ${message}`);
11
6
  }
12
- /**
13
- * Identify MCP traffic.
14
- *
15
- * Without this every MCP request arrives as Node's default agent - "node" -
16
- * which is the single largest bucket in api_usage_logs (2.78M requests across
17
- * 182 keys) and indistinguishable from any other script. That made MCP adoption
18
- * unmeasurable: we could not tell whether an agent-driven user activates faster,
19
- * consumes more, or converts better than someone hand-rolling curl.
20
- *
21
- * Versioned so a bad release can be isolated in the logs.
22
- */
23
7
  export const CITO_MCP_USER_AGENT = `cito-mcp/${PACKAGE_VERSION} (+https://citoapi.com)`;
24
8
  export function authHeaders(apiKey, extra) {
25
9
  return {
@@ -43,17 +27,11 @@ export function parseRateLimitHeaders(headers) {
43
27
  resetAt: resetRaw ?? null,
44
28
  };
45
29
  }
46
- /**
47
- * Build absolute URL under api base.
48
- * path must start with `/`. Query values skip null/undefined.
49
- * Arrays become repeated query keys.
50
- */
51
30
  export function buildUrl(baseUrl, path, query) {
52
31
  if (!path.startsWith('/')) {
53
32
  throw new Error(`path must start with /: ${path}`);
54
33
  }
55
34
  const base = baseUrl.replace(/\/+$/, '');
56
- // LoL-style absolute API paths: if path already includes /api/v1, use origin only
57
35
  let originBase = base;
58
36
  if (path.startsWith('/api/v1/') && base.endsWith('/api/v1')) {
59
37
  originBase = base.slice(0, -'/api/v1'.length) || base;
@@ -78,14 +56,12 @@ export function buildUrl(baseUrl, path, query) {
78
56
  const q = qs.toString();
79
57
  return `${originBase}${path}${q ? `?${q}` : ''}`;
80
58
  }
81
- /** Pretty-print JSON; truncate oversized bodies with an agent-actionable note. */
82
59
  export function present(text, maxBytes = DEFAULT_MAX_RESPONSE_BYTES) {
83
60
  let out = text;
84
61
  try {
85
62
  out = JSON.stringify(JSON.parse(text), null, 2);
86
63
  }
87
64
  catch {
88
- // Not JSON — return as-is.
89
65
  }
90
66
  if (out.length > maxBytes) {
91
67
  return (`${out.slice(0, maxBytes)}\n\n` +
@@ -142,7 +118,6 @@ export async function parallelGet(ctx, paths) {
142
118
  }));
143
119
  return Object.fromEntries(results);
144
120
  }
145
- /** Best-effort row extraction across Cito response shapes. */
146
121
  export function extractRows(data) {
147
122
  if (data == null)
148
123
  return [];
@@ -151,8 +126,6 @@ export function extractRows(data) {
151
126
  if (typeof data !== 'object')
152
127
  return [];
153
128
  const obj = data;
154
- // Order matters: UFC /ufc/live nests real bouts under liveBouts while also
155
- // shipping supervisor `events` (no fighter names). Prefer bout-like keys first.
156
129
  for (const key of [
157
130
  'data',
158
131
  'liveBouts',
@@ -174,7 +147,6 @@ export function extractRows(data) {
174
147
  if (Array.isArray(obj[key]))
175
148
  return obj[key];
176
149
  }
177
- // Nested data.matches / data.items
178
150
  if (obj.data && typeof obj.data === 'object' && !Array.isArray(obj.data)) {
179
151
  return extractRows(obj.data);
180
152
  }
@@ -186,21 +158,14 @@ export function asRecord(value) {
186
158
  }
187
159
  return null;
188
160
  }
189
- /**
190
- * Peel Cito `{ success, data: T }` (and one nested `data`) so tools see the entity,
191
- * not the envelope. Arrays and primitives pass through. Critical for UFC bouts/fighters
192
- * where fighters[] / name live under data, not the top-level response.
193
- */
194
161
  export function unwrapPayload(value) {
195
162
  let cur = value;
196
163
  for (let depth = 0; depth < 3; depth += 1) {
197
164
  const rec = asRecord(cur);
198
165
  if (!rec)
199
166
  return cur;
200
- // Classic withMeta: { success: true, data: { ...entity } }
201
167
  if ('data' in rec && rec.data != null && typeof rec.data === 'object' && !Array.isArray(rec.data)) {
202
168
  const inner = rec.data;
203
- // Prefer entity-shaped inner objects over list wrappers
204
169
  const looksLikeEntity = 'id' in inner ||
205
170
  'slug' in inner ||
206
171
  'name' in inner ||
package/dist/envelope.js CHANGED
@@ -1,7 +1,4 @@
1
- /**
2
- * Canonical JSON envelope for every curated cito-mcp tool response.
3
- * Agents branch on `ok` / `error.code` / `partial[]` without scraping prose.
4
- */
1
+ import { scrubInternalFields } from './scrub.js';
5
2
  export function isOk(e) {
6
3
  return e.ok === true;
7
4
  }
@@ -64,8 +61,6 @@ export function mapHttpToCode(status, hints) {
64
61
  }
65
62
  if (status === 400 && hints?.ambiguous)
66
63
  return 'AMBIGUOUS_ENTITY';
67
- // 422 is FastAPI's validation status (tennis). It was falling through to
68
- // UPSTREAM, which is marked retryable, so an agent retried a bad argument.
69
64
  if (status === 400 || status === 422)
70
65
  return 'VALIDATION';
71
66
  if (status >= 500 || status === 0)
@@ -95,7 +90,7 @@ export function successEnvelope(args) {
95
90
  meta.entities = args.entities;
96
91
  const out = {
97
92
  ok: true,
98
- data: args.data,
93
+ data: scrubInternalFields(args.data),
99
94
  meta,
100
95
  };
101
96
  if (args.pagination)
@@ -135,7 +130,7 @@ export function errorEnvelope(args) {
135
130
  if (args.hint)
136
131
  error.hint = args.hint;
137
132
  if (args.details)
138
- error.details = args.details;
133
+ error.details = scrubInternalFields(args.details);
139
134
  return {
140
135
  ok: false,
141
136
  data: null,
@@ -154,10 +149,6 @@ export function partialFromRejection(section, err) {
154
149
  ...(err.httpStatus !== undefined ? { httpStatus: err.httpStatus } : {}),
155
150
  };
156
151
  }
157
- /**
158
- * Enforce MAX_ENVELOPE_CHARS with structural shrink (never mid-slice JSON).
159
- * Prefers trimming data.items when present.
160
- */
161
152
  export function sealEnvelope(envelope) {
162
153
  let text = JSON.stringify(envelope);
163
154
  if (text.length <= MAX_ENVELOPE_CHARS)
@@ -203,7 +194,6 @@ export function sealEnvelope(envelope) {
203
194
  },
204
195
  };
205
196
  }
206
- /** Shared tool output for OpenAI plugin scan. Matches the JSON envelope. */
207
197
  export const ENVELOPE_OUTPUT_SCHEMA = {
208
198
  type: 'object',
209
199
  additionalProperties: true,
package/dist/http.js CHANGED
@@ -1,10 +1,3 @@
1
- /**
2
- * Hosted Streamable HTTP helpers.
3
- *
4
- * stdio is unchanged. This is only the public HTTPS surface:
5
- * pathname matching, per-request API key, CORS, Smithery well-known docs.
6
- * Never log the key.
7
- */
8
1
  import { allTools } from './tools/index.js';
9
2
  import { toolListing } from './tools/types.js';
10
3
  import { PACKAGE_VERSION } from './version.js';
@@ -34,10 +27,6 @@ function headerValue(headers, name) {
34
27
  return (raw[0] ?? '').trim();
35
28
  return typeof raw === 'string' ? raw.trim() : '';
36
29
  }
37
- /**
38
- * Per-request Cito key. Header first (Smithery x-from), then Bearer, then query.
39
- * Query is last so nginx access logs are not the intended path.
40
- */
41
30
  export function extractApiKey(req) {
42
31
  const fromHeader = headerValue(req.headers, 'x-api-key') || headerValue(req.headers, 'x-cito-api-key');
43
32
  if (fromHeader)
@@ -62,7 +51,6 @@ export function corsHeaders() {
62
51
  }
63
52
  const MCP_ACCEPT = 'application/json, text/event-stream';
64
53
  const LEGACY_PROTOCOL = '2025-11-25';
65
- /** Node reads Accept from rawHeaders; mutating headers.accept alone is not enough. */
66
54
  export function setIncomingHeader(req, name, value) {
67
55
  req.headers[name.toLowerCase()] = value;
68
56
  const raw = req.rawHeaders;
@@ -77,7 +65,6 @@ export function setIncomingHeader(req, name, value) {
77
65
  }
78
66
  raw.push(name, value);
79
67
  }
80
- /** SDK 406s POSTs that omit MCP Accept. OpenAI's scanner often omits it. */
81
68
  export function ensureMcpAccept(headers, req) {
82
69
  const raw = headers.accept ?? headers.Accept;
83
70
  const value = Array.isArray(raw) ? raw.join(',') : typeof raw === 'string' ? raw : '';
@@ -89,7 +76,6 @@ export function ensureMcpAccept(headers, req) {
89
76
  else
90
77
  headers.accept = MCP_ACCEPT;
91
78
  }
92
- /** OpenAI Scan Tools sends 2026-07-28; this SDK only speaks 2025-era. */
93
79
  export function rewriteMcpProtocolMessage(body) {
94
80
  if (!body || typeof body !== 'object')
95
81
  return body;
@@ -109,7 +95,6 @@ export function rewriteMcpProtocolMessage(body) {
109
95
  }
110
96
  return body;
111
97
  }
112
- /** Smithery session config: API key as x-api-key, not OAuth. */
113
98
  export function mcpConfigSchema() {
114
99
  return {
115
100
  $schema: 'https://json-schema.org/draft/2020-12/schema',
@@ -127,7 +112,6 @@ export function mcpConfigSchema() {
127
112
  },
128
113
  };
129
114
  }
130
- /** Scan fallback if Smithery cannot complete initialize. No OAuth. */
131
115
  export function mcpServerCard() {
132
116
  return {
133
117
  serverInfo: {
package/dist/index.js CHANGED
@@ -1,13 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * cito-mcp — curated MCP server for the Cito esports API.
4
- *
5
- * 14 outcome tools (not OpenAPI mass-generation). stdio by default;
6
- * `--http <port>` serves a stateless Streamable HTTP endpoint.
7
- *
8
- * Env: CITO_API_KEY (stdio / local). Hosted HTTP reads x-api-key per request.
9
- * Optional: CITO_API_BASE, CITO_MCP_LISTEN (default 127.0.0.1).
10
- */
11
2
  import { AsyncLocalStorage } from 'node:async_hooks';
12
3
  import { createServer as createHttpServer } from 'node:http';
13
4
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
@@ -21,14 +12,6 @@ import { allTools, getTool } from './tools/index.js';
21
12
  import { runTool, toolListing } from './tools/types.js';
22
13
  import { PACKAGE_VERSION } from './version.js';
23
14
  import { MCP_CONFIG_PATH, MCP_PATH, MCP_SERVER_CARD_PATH, corsHeaders, ensureMcpAccept, extractApiKey, rewriteMcpProtocolMessage, setIncomingHeader, mcpConfigSchema, mcpServerCard, requestPathname, } from './http.js';
24
- /**
25
- * Subcommands run and exit before any transport exists.
26
- *
27
- * `install` writes editor config and prints to stdout, which would corrupt the
28
- * JSON-RPC channel if it ran alongside the server — hence the early return.
29
- * Handled here rather than in a second binary so the documented entry point
30
- * stays `npx cito-mcp`.
31
- */
32
15
  if (process.argv[2] === 'install') {
33
16
  const { runInstall } = await import('./install.js');
34
17
  process.exit(await runInstall(process.argv.slice(3)));
@@ -38,26 +21,8 @@ const requestAuth = new AsyncLocalStorage();
38
21
  function currentApiKey() {
39
22
  return requestAuth.getStore()?.apiKey || ENV_API_KEY;
40
23
  }
41
- /**
42
- * Tools that need no API key. These must keep working with the server
43
- * unconfigured, so a user can add it and immediately see what it does.
44
- */
45
24
  const OFFLINE_TOOLS = new Set(['list_capabilities']);
46
- /**
47
- * Missing key is NOT fatal.
48
- *
49
- * This used to process.exit(1) before the transport was even created, so the
50
- * handshake never completed and the client showed only "server failed to
51
- * start" — no tool list, no reason, nothing the model could relay. The user's
52
- * first experience of a mistyped env var was a dead server.
53
- *
54
- * Now the server boots, completes initialize, and advertises its full catalog.
55
- * Tools that need the API return a structured MISSING_API_KEY envelope with
56
- * recovery steps, which the model can read out; list_capabilities keeps working
57
- * offline so the server is browsable before it is configured.
58
- */
59
25
  if (!ENV_API_KEY) {
60
- // stderr only — stdout is the JSON-RPC channel and must stay clean.
61
26
  console.error('[cito-mcp] CITO_API_KEY is not set. Serving tool catalog only; ' +
62
27
  'API-backed tools will return MISSING_API_KEY until a key is provided ' +
63
28
  '(env CITO_API_KEY, or x-api-key on hosted HTTP). ' +
@@ -65,8 +30,6 @@ if (!ENV_API_KEY) {
65
30
  }
66
31
  const API_BASE = (process.env.CITO_API_BASE || DEFAULT_API_BASE).replace(/\/+$/, '');
67
32
  const ctx = {
68
- // Empty string when unconfigured; the dispatcher blocks API-backed tools
69
- // before any request is attempted, so this is never sent as a credential.
70
33
  apiKey: ENV_API_KEY,
71
34
  baseUrl: API_BASE,
72
35
  };
@@ -94,8 +57,6 @@ function createCitoServer() {
94
57
  recover: ['Call list_capabilities to see available tools'],
95
58
  }));
96
59
  }
97
- // Fail the CALL, not the process. The model gets an actionable envelope it
98
- // can relay verbatim instead of the client reporting a dead server.
99
60
  const apiKey = currentApiKey();
100
61
  if (!apiKey && !OFFLINE_TOOLS.has(name)) {
101
62
  return toMcpResult(errorEnvelope({
package/dist/install.js CHANGED
@@ -1,25 +1,3 @@
1
- /**
2
- * `npx cito-mcp install` — write the server config for whichever MCP clients
3
- * are actually installed.
4
- *
5
- * Why this exists: the documented one-liner
6
- *
7
- * claude mcp add cito -e CITO_API_KEY=… -- npx -y cito-mcp
8
- *
9
- * is broken in PowerShell 5.1, which strips the bare `--`. Because `-e` is a
10
- * variadic option, it then swallows `npx -y cito-mcp` as extra env values and
11
- * the CLI dies on "unknown option '-y'". Quoting the separator fixes it, but
12
- * expecting every Windows user to know that is not onboarding — it is a trap.
13
- * A subcommand has no separator to mangle and works identically everywhere.
14
- *
15
- * Rules this follows, because it edits files the user did not open:
16
- * - only touch clients that are already installed
17
- * - back up before the first write of a run
18
- * - merge into existing config; never rewrite a file wholesale
19
- * - idempotent: re-running replaces our entry and nothing else
20
- * - --dry-run prints the plan and writes nothing
21
- * - never print the full API key
22
- */
23
1
  import { execFileSync } from 'node:child_process';
24
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs';
25
3
  import { homedir, platform } from 'node:os';
@@ -80,7 +58,6 @@ function clientsFor() {
80
58
  label: 'Grok CLI',
81
59
  configPath: join(home, '.grok', 'config.toml'),
82
60
  detectPath: join(home, '.grok'),
83
- // Same [mcp_servers.x] / [mcp_servers.x.env] layout as Codex.
84
61
  format: 'toml-mcp_servers',
85
62
  },
86
63
  ];
@@ -90,7 +67,6 @@ export function maskKey(key) {
90
67
  return '****';
91
68
  return `${key.slice(0, 9)}…${key.slice(-4)}`;
92
69
  }
93
- /** The stdio entry every JSON-based client understands. */
94
70
  function serverEntry(key, base) {
95
71
  return {
96
72
  type: 'stdio',
@@ -113,11 +89,6 @@ function readJson(path) {
113
89
  return null;
114
90
  }
115
91
  }
116
- /**
117
- * Escape a TOML basic string. Only the characters TOML actually requires —
118
- * an API key with a quote or backslash would otherwise produce a file the
119
- * client cannot parse.
120
- */
121
92
  function tomlString(v) {
122
93
  return `"${v.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
123
94
  }
@@ -135,11 +106,6 @@ function tomlBlock(key, base) {
135
106
  ``,
136
107
  ].join('\n');
137
108
  }
138
- /**
139
- * Replace an existing [mcp_servers.cito] block (and its .env subtable) or
140
- * append a new one. Deliberately conservative: it only recognises the exact
141
- * headers it writes, so a hand-edited file is appended to rather than mangled.
142
- */
143
109
  export function upsertTomlBlock(existing, key, base) {
144
110
  const block = tomlBlock(key, base);
145
111
  const header = new RegExp(`^\\[mcp_servers\\.${SERVER_NAME}(\\.[A-Za-z_]+)?\\]\\s*$`);
@@ -160,16 +126,8 @@ export function upsertTomlBlock(existing, key, base) {
160
126
  const body = kept.join('\n').replace(/\n{3,}$/, '\n\n').replace(/\s*$/, '');
161
127
  return (body ? `${body}\n\n` : '') + block;
162
128
  }
163
- /**
164
- * Claude Code owns the schema of ~/.claude.json and stores far more than MCP
165
- * config in it, so let its own CLI do the write when it is on PATH. Direct
166
- * editing is the fallback, not the default.
167
- */
168
129
  function tryClaudeCli(key, base, dryRun) {
169
130
  const json = JSON.stringify(serverEntry(key, base));
170
- // On Windows `claude` is a .cmd shim, which execFileSync cannot spawn
171
- // directly — it needs a shell. Quoting matters there because the JSON
172
- // payload contains spaces and double quotes.
173
131
  const isWin = platform() === 'win32';
174
132
  const run = (args) => {
175
133
  if (!isWin) {
@@ -299,8 +257,6 @@ export async function runInstall(argv) {
299
257
  console.error(`Valid: ${all.map((c) => c.id).join(', ')}`);
300
258
  return 1;
301
259
  }
302
- // An explicit --client is a instruction, not a guess: honour it even if we
303
- // cannot detect the client (fresh install, portable install, unusual path).
304
260
  const selected = args.only.length
305
261
  ? all.filter((c) => args.only.includes(c.id))
306
262
  : all.filter((c) => existsSync(c.detectPath));
@@ -1,7 +1,3 @@
1
- /**
2
- * MCP server instructions — agent operating manual (embedded at connect).
3
- * Keep under ~1.5k tokens. Tool names match the curated catalog (no cito_ prefix).
4
- */
5
1
  export const SERVER_INSTRUCTIONS = `# Cito MCP — agent operating manual
6
2
 
7
3
  You are connected to Cito esports data (read-only). Prefer curated outcome tools over raw REST. Production apps must call Cito REST with the user's API key; use this MCP to design, prototype, and resolve IDs — not as a multi-tenant runtime bus.
package/dist/scrub.js ADDED
@@ -0,0 +1,67 @@
1
+ const INTERNAL_KEYS = new Set([
2
+ 'emptyCardHeal',
3
+ 'llmValidation',
4
+ 'workerHostHint',
5
+ 'scrapeMeta',
6
+ 'fetchMeta',
7
+ 'cacheKey',
8
+ 'failureSteps',
9
+ 'htmlSnapshotFile',
10
+ 'latestHtmlSnapshotFile',
11
+ 'lastSyncedAt',
12
+ 'sourceUrl',
13
+ 'sourceUrls',
14
+ 'sourceIds',
15
+ 'wikiUrl',
16
+ 'wiki_url',
17
+ 'jsonLd',
18
+ ]);
19
+ const SCRAPE_PARENTS = new Set(['dataAvailability', 'health']);
20
+ const SCRAPE_KEYS = new Set(['strategy', 'samples']);
21
+ const MEDIA_PARENT = /image|photo|portrait|picture/i;
22
+ const ERROR_KEYS = new Set(['errors', 'error', 'lastError', 'bodyPreview']);
23
+ const INTERNAL_ERROR_TEXT = /prisma|raw query failed|invocation|ECONNREFUSED|\bat \S+ \(/i;
24
+ export const INTERNAL_ERROR_PLACEHOLDER = 'internal_error';
25
+ function isInternal(key, parentKey) {
26
+ if (key === 'sourceUrl' && MEDIA_PARENT.test(parentKey))
27
+ return false;
28
+ return INTERNAL_KEYS.has(key) || (SCRAPE_KEYS.has(key) && SCRAPE_PARENTS.has(parentKey));
29
+ }
30
+ function scrub(node, parentKey, inError) {
31
+ if (typeof node === 'string') {
32
+ return inError && INTERNAL_ERROR_TEXT.test(node) ? INTERNAL_ERROR_PLACEHOLDER : node;
33
+ }
34
+ if (Array.isArray(node)) {
35
+ let out = null;
36
+ for (let i = 0; i < node.length; i++) {
37
+ const value = node[i];
38
+ const next = scrub(value, parentKey, inError);
39
+ if (next !== value) {
40
+ out ??= node.slice();
41
+ out[i] = next;
42
+ }
43
+ }
44
+ return out ?? node;
45
+ }
46
+ if (!node || typeof node !== 'object' || node instanceof Date)
47
+ return node;
48
+ const src = node;
49
+ let out = null;
50
+ for (const key of Object.keys(src)) {
51
+ const value = src[key];
52
+ if (isInternal(key, parentKey)) {
53
+ out ??= { ...src };
54
+ delete out[key];
55
+ continue;
56
+ }
57
+ const next = scrub(value, key, inError || ERROR_KEYS.has(key));
58
+ if (next !== value) {
59
+ out ??= { ...src };
60
+ out[key] = next;
61
+ }
62
+ }
63
+ return out ?? node;
64
+ }
65
+ export function scrubInternalFields(value) {
66
+ return scrub(value, '', false);
67
+ }
package/dist/tools/cs2.js CHANGED
@@ -1,20 +1,6 @@
1
- /**
2
- * Dedicated CS2 esports analytics & live scorebot MCP tools.
3
- *
4
- * Exposes deep CS2 domain data from the 161 backend endpoints:
5
- * - Live real-time scoreboards & round state
6
- * - Round-by-round tactical economies & buy classifications
7
- * - First blood / opening duels per player & map
8
- * - 1vX clutch success rates (1v1 through 1v5)
9
- * - Team map pool analytics (Mirage, Inferno, Nuke, Dust2, Ancient, Anubis, Vertigo)
10
- * - Sub-second utility & flashbang efficiency leaderboard
11
- * - Match map pick/ban veto sequence
12
- * - Pro roster changes & transfers
13
- */
14
1
  import { asRecord, clampInt, fetchJson, unwrapPayload, } from '../client.js';
15
2
  import { errorEnvelope, mapHttpToCode, newRequestId, successEnvelope, } from '../envelope.js';
16
3
  import { limitSchema, READ_TOOL_ANNOTATIONS, stringSchema, } from './types.js';
17
- /** Unwraps Cito envelope whether inner entity is an object or array */
18
4
  function unwrap(val) {
19
5
  const p = unwrapPayload(val);
20
6
  const r = asRecord(p);
@@ -24,10 +10,6 @@ function unwrap(val) {
24
10
  return r.items;
25
11
  return p;
26
12
  }
27
- /**
28
- * 1. cs2_live_scoreboard
29
- * Real-time round state, bomb status, and player combat stats for an active match.
30
- */
31
13
  export const cs2LiveScoreboard = {
32
14
  name: 'cs2_live_scoreboard',
33
15
  description: 'Live real-time in-game scoreboard for an active CS2 match. Returns current round, bomb status, team scores, and individual player stats (K/A/D, ADR, HS%, alive status, HP, armor, weapon, and equipment value).',
@@ -75,10 +57,6 @@ export const cs2LiveScoreboard = {
75
57
  });
76
58
  },
77
59
  };
78
- /**
79
- * 2. cs2_round_economy
80
- * Tactical round-by-round buy classification and equipment values for a map.
81
- */
82
60
  export const cs2RoundEconomy = {
83
61
  name: 'cs2_round_economy',
84
62
  description: 'Round-by-round tactical economy for a CS2 map. Classifies every round buy type (Full Buy, Force Buy, Semi-Eco, Full Eco), team equipment spend, freeze-time equipment values, and loss bonus counter.',
@@ -128,10 +106,6 @@ export const cs2RoundEconomy = {
128
106
  });
129
107
  },
130
108
  };
131
- /**
132
- * 3. cs2_opening_duels
133
- * First blood / opening kill statistics by player or overall leaderboard.
134
- */
135
109
  export const cs2OpeningDuels = {
136
110
  name: 'cs2_opening_duels',
137
111
  description: 'First blood and opening duel statistics. Pass playerId to view a pro player\'s First Kills (FK), First Deaths (FD), and opening duel conversion %, or omit playerId to view the global CS2 opening duel leaderboard.',
@@ -172,10 +146,6 @@ export const cs2OpeningDuels = {
172
146
  });
173
147
  },
174
148
  };
175
- /**
176
- * 4. cs2_clutches
177
- * 1vX clutch situation success records (1v1 through 1v5).
178
- */
179
149
  export const cs2Clutches = {
180
150
  name: 'cs2_clutches',
181
151
  description: '1vX clutch situation success records. Pass playerId for a specific player\'s clutch breakdown (1v1, 1v2, 1v3, 1v4, 1v5 attempted vs won), or omit to view the global clutch leaderboard.',
@@ -216,17 +186,13 @@ export const cs2Clutches = {
216
186
  });
217
187
  },
218
188
  };
219
- /**
220
- * 5. cs2_team_map_stats
221
- * Team performance across the competitive map pool.
222
- */
223
189
  export const cs2TeamMapStats = {
224
190
  name: 'cs2_team_map_stats',
225
191
  description: 'Team win rates, round win rates, and CT/T side win splits across the competitive map pool (Mirage, Inferno, Nuke, Dust2, Ancient, Anubis, Vertigo) over the last 30 or 90 days.',
226
192
  inputSchema: {
227
193
  type: 'object',
228
194
  properties: {
229
- teamId: stringSchema('Team ID or slug (e.g. "hltv-team-4608", "cs2-team-4608", or "natus-vincere"). Required.', 'hltv-team-4608'),
195
+ teamId: stringSchema('Team ID or slug (e.g. "cs2-team-4608" or "natus-vincere"). Required.', 'cs2-team-4608'),
230
196
  days: {
231
197
  type: 'integer',
232
198
  description: 'Timeframe in days: 30, 90, 180, or 365 (default 90).',
@@ -272,10 +238,6 @@ export const cs2TeamMapStats = {
272
238
  });
273
239
  },
274
240
  };
275
- /**
276
- * 6. cs2_utility_leaderboard
277
- * Sub-second cached leaderboard for flashbang & utility efficiency.
278
- */
279
241
  export const cs2UtilityLeaderboard = {
280
242
  name: 'cs2_utility_leaderboard',
281
243
  description: 'Sub-second cached leaderboard of CS2 utility and flashbang efficiency. Returns pro player rankings for effective blind duration per flash, enemy blind time, flashes thrown, and grenade ADR.',
@@ -312,10 +274,6 @@ export const cs2UtilityLeaderboard = {
312
274
  });
313
275
  },
314
276
  };
315
- /**
316
- * 7. cs2_veto_sequence
317
- * Map pick/ban sequence and veto history for a match.
318
- */
319
277
  export const cs2VetoSequence = {
320
278
  name: 'cs2_veto_sequence',
321
279
  description: 'Map pick/ban veto sequence for a CS2 match. Shows which team banned which map, map picks, and decider map in chronological order.',
@@ -365,10 +323,6 @@ export const cs2VetoSequence = {
365
323
  });
366
324
  },
367
325
  };
368
- /**
369
- * 8. cs2_roster_transfers
370
- * Recent professional roster changes, benchings, and transfers.
371
- */
372
326
  export const cs2RosterTransfers = {
373
327
  name: 'cs2_roster_transfers',
374
328
  description: 'Recent professional CS2 roster changes, player benchings, stand-ins, and team transfers.',
@@ -408,7 +362,6 @@ export const cs2RosterTransfers = {
408
362
  });
409
363
  },
410
364
  };
411
- /** All curated CS2 tools */
412
365
  export const cs2Tools = [
413
366
  cs2LiveScoreboard,
414
367
  cs2RoundEconomy,
@@ -11,7 +11,6 @@ import { insightTools } from './insight.js';
11
11
  import { cs2Tools } from './cs2.js';
12
12
  import { tournamentTools } from './tournaments.js';
13
13
  import { scheduleTools } from './schedule.js';
14
- /** Curated outcome-tool catalog. Order matches preferred cold-start ladder. */
15
14
  export const allTools = [
16
15
  ...metaTools.filter((t) => t.name === 'list_capabilities' || t.name === 'api_health'),
17
16
  ...resolveTools,
@@ -22,20 +21,10 @@ export const allTools = [
22
21
  ...standingsTools,
23
22
  ...rankingsTools,
24
23
  ...leaderboardTools,
25
- // Tennis depth added after the coverage audit: odds, tournament discovery and
26
- // the daily schedule were reachable only through call_api.
27
- //
28
- // oddsTools REMOVED 2026-09-13. Tennis odds were withdrawn from the API
29
- // (/tennis/odds/* is unmounted), so the tool would only ever call a 404.
30
- // Betting data concentrates the legal risk and is the subject of every
31
- // significant dispute researched — Swish Analytics v OddsJam is a data vendor
32
- // suing rivals over scraped odds. See docs/tennis-risk-register.md.
33
- // To restore: re-add `...oddsTools,` here and remount odds_router in the API.
34
24
  ...tournamentTools,
35
25
  ...scheduleTools,
36
26
  ...insightTools,
37
27
  ...cs2Tools,
38
- // Escape-hatch pair last: discover routes, then call one.
39
28
  ...metaTools.filter((t) => t.name === 'list_routes' || t.name === 'call_api'),
40
29
  ];
41
30
  export function getTool(name) {