cito-mcp 0.4.4 → 0.4.6

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
@@ -535,7 +535,7 @@ claude mcp add cito -e CITO_API_KEY=cito_… "--" npx -y cito-mcp
535
535
  ### Semver expectations
536
536
 
537
537
  - **0.2.4** is a **major surface break** vs 0.1 (tool rename + removal of OpenAPI mass-generation).
538
- - Further 0.2.x patches may refine envelopes and composite quality without renaming the 15 tools.
538
+ - Further 0.2.x patches may refine envelopes and composite quality without renaming the shipped tools. (This line previously said "15 tools" while the catalog shipped 42 — the count here is intentionally not restated, because a number duplicated in prose drifts the moment the catalog changes.)
539
539
  - Promoting Fortnite (or other titles) into the primary `game` enum would be a minor feature bump with catalog/docs updates.
540
540
 
541
541
  ---
@@ -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` +
@@ -126,6 +102,8 @@ export async function fetchJson(ctx, path, opts) {
126
102
  catch {
127
103
  data = text;
128
104
  }
105
+ if (!opts?.raw && response.ok && isTennisPath(path))
106
+ data = flattenTennisEnvelope(data);
129
107
  return {
130
108
  ok: response.ok,
131
109
  status: response.status,
@@ -142,7 +120,6 @@ export async function parallelGet(ctx, paths) {
142
120
  }));
143
121
  return Object.fromEntries(results);
144
122
  }
145
- /** Best-effort row extraction across Cito response shapes. */
146
123
  export function extractRows(data) {
147
124
  if (data == null)
148
125
  return [];
@@ -151,8 +128,6 @@ export function extractRows(data) {
151
128
  if (typeof data !== 'object')
152
129
  return [];
153
130
  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
131
  for (const key of [
157
132
  'data',
158
133
  'liveBouts',
@@ -174,7 +149,6 @@ export function extractRows(data) {
174
149
  if (Array.isArray(obj[key]))
175
150
  return obj[key];
176
151
  }
177
- // Nested data.matches / data.items
178
152
  if (obj.data && typeof obj.data === 'object' && !Array.isArray(obj.data)) {
179
153
  return extractRows(obj.data);
180
154
  }
@@ -186,21 +160,51 @@ export function asRecord(value) {
186
160
  }
187
161
  return null;
188
162
  }
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
- */
163
+ const TENNIS_LIST_META_KEYS = new Set(['count', 'limit', 'total', 'page', 'totalPages', 'hasNext', 'hasPrev']);
164
+ function isTennisPath(path) {
165
+ return /^(\/api\/v1)?\/tennis(\/|\?|$)/.test(path);
166
+ }
167
+ function snakeKey(key) {
168
+ return key.replace(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`);
169
+ }
170
+ export function flattenTennisEnvelope(body) {
171
+ const env = asRecord(body);
172
+ if (!env || !('data' in env) || 'items' in env)
173
+ return body;
174
+ const meta = asRecord(env.meta);
175
+ if (!meta)
176
+ return body;
177
+ const success = env.success ?? true;
178
+ if (Array.isArray(env.data)) {
179
+ const out = { success, items: env.data };
180
+ for (const [key, value] of Object.entries(meta)) {
181
+ if (!TENNIS_LIST_META_KEYS.has(key))
182
+ out[snakeKey(key)] = value;
183
+ }
184
+ out.total = typeof meta.total === 'number' ? meta.total : env.data.length;
185
+ out.page = typeof meta.page === 'number' ? meta.page : 1;
186
+ out.page_size = typeof meta.limit === 'number' ? meta.limit : env.data.length;
187
+ if (typeof meta.totalPages === 'number')
188
+ out.total_pages = meta.totalPages;
189
+ if (typeof meta.hasNext === 'boolean')
190
+ out.has_next = meta.hasNext;
191
+ if (typeof meta.hasPrev === 'boolean')
192
+ out.has_prev = meta.hasPrev;
193
+ return out;
194
+ }
195
+ const resource = asRecord(env.data);
196
+ if (resource)
197
+ return { success, ...resource };
198
+ return body;
199
+ }
194
200
  export function unwrapPayload(value) {
195
201
  let cur = value;
196
202
  for (let depth = 0; depth < 3; depth += 1) {
197
203
  const rec = asRecord(cur);
198
204
  if (!rec)
199
205
  return cur;
200
- // Classic withMeta: { success: true, data: { ...entity } }
201
206
  if ('data' in rec && rec.data != null && typeof rec.data === 'object' && !Array.isArray(rec.data)) {
202
207
  const inner = rec.data;
203
- // Prefer entity-shaped inner objects over list wrappers
204
208
  const looksLikeEntity = 'id' in inner ||
205
209
  'slug' in inner ||
206
210
  '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,81 +1,77 @@
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
- export const SERVER_INSTRUCTIONS = `# Cito MCP — agent operating manual
6
-
7
- 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.
8
-
9
- ## Hard rules
10
-
11
- 1. **Never invent IDs.** Do not guess matchId, gameId, playerId, team slug, eventId, or boutId. Always obtain IDs from a prior tool result (resolve, live, schedule, search, or list). If the user gives a name ("T1", "s1mple", "UFC 300"), call resolve_entity/search_entities first.
12
- 2. **Resolve before deep.** Name → ID/slug → summary/page → deep stats. Skip resolve only when the user already provided a Cito ID/slug.
13
- 3. **One screen, one composite.** Prefer a page/summary tool over stitching 4–6 thin GETs.
14
- 4. **Honor the envelope.** Parse ok, data, pagination, partial, error, meta.rateLimit, meta.entities. Partial section failures are not total failures — use successful sections.
15
- 5. **Read-only.** All curated tools are safe to retry except where error.retryable is false for bad args / entitlement.
16
-
17
- ## Game parameter
18
-
19
- game enum (unless a tool documents otherwise): lol | cs2 | dota2 | ufc | cod | tennis | all
20
-
21
- - Default when the user named one title: that game. Default for "what's live?" / multi-title: omit game or all on tools that accept it.
22
- - On UNSUPPORTED_GAME / 403 for a title: stop retrying that game; call api_health; report plan gaps.
23
- - Fortnite and other titles may appear via call_api/resources; do not assume they are in the curated enum until list_capabilities says so.
24
-
25
- ## Preferred tool order
26
-
27
- 1. Unsure → list_capabilities (filter by game or job). Optional: api_health for key/tier/included games.
28
- 2. Name without ID → resolve_entity (best match) or search_entities (browse). Reuse returned id/slug.
29
- 3. Live / upcoming → live_matches; upcoming_schedule.
30
- 4. Match UI / recap → match_summary first. match_details only for timelines, full maps, demos, live state.
31
- 5. Team page → team_profile. Player form → player_profile. Tables/ranks → standings.
32
- 6. Pre-match → match_preview. Rivalry → head_to_head. Event / fight-night card → event_card.
33
- 7. Escape hatch → call_api (allowlisted path prefixes; prefer GET). Prefer curated tools.
34
-
35
- Mnemonic: resolve → live/schedule → summary → deep.
36
-
37
- ## Parallel vs sequence
38
-
39
- Parallel-safe: independent reads (team_profile A ∥ team_profile B; live_matches ∥ api_health). Cap ~3–5 concurrent agent tools; check meta.rateLimit.remaining.
40
-
41
- Serial required: resolve → detail; pagination (cursor from page N only); live board → selected match deep-dive.
42
-
43
- Prefer server-side fan-out inside composites (partial[] recovery) over agent N+1.
44
-
45
- ## Pagination
46
-
47
- Lists use pagination: { limit, offset?, total?, hasMore, nextCursor, prevCursor }. Default limit 20, max 50. Loop with cursor: pagination.nextCursor and the same filters. Never invent cursors. Empty list is ok:true with items:[].
48
-
49
- ## Errors
50
-
51
- ok:false → read error.code (VALIDATION, NOT_FOUND, UNSUPPORTED_GAME, RATE_LIMIT, UNAUTHORIZED, UPSTREAM, PATH_NOT_ALLOWED, NOT_IMPLEMENTED). Follow error.recover[]. Retry only when retryable.
52
-
53
- ok:true with partial[] → use successful sections; do not treat partial as total fail.
54
- ok:true with meta.warnings[] → filters or depth degraded; do not assume ignored filters applied.
55
- resolve_entity ambiguity is soft: ok:true with data.needsDisambiguation and data.candidates — pick a candidate; do not wait for AMBIGUOUS_ENTITY.
56
-
57
- ## Recipes
58
-
59
- Live board: api_health (optional) → live_matches → match_summary for selected matchId.
60
- UFC empty live (count=0): read section.note / emptyReason / health (workerAlive, lag) / supervisor / nextCard — do not claim "API offline" without workerAlive/lag; never invent matchups from supervisor shells.
61
- Team page: resolve_entity {type:team} → team_profile.
62
- Player card: resolve_entity {type:player|fighter} → player_profile.
63
- Fight night / event card: resolve_entity {type:event} → event_card {includeMatches:true} → match_preview for a featured bout.
64
- Match preview (named sides): resolve_entity each side (optional) → match_preview {teamA, teamB}.
65
- App scaffold: list_capabilities ∥ api_health ∥ live_matches, then one composite per screen.
66
-
67
- ## Resources
68
-
69
- - cito://llms.txt — product context when available
70
- - cito://capabilities — catalog summary
71
- - cito://openapi.json — optional public OpenAPI fetch for typed clients
72
-
73
- ## What not to do
74
-
75
- - Invent match/player/team IDs from memory.
76
- - Parallel-paginate the same list with different cursors.
77
- - N+1 match_details for every live row.
78
- - Retry UNSUPPORTED_GAME or VALIDATION unchanged.
79
- - Ship production traffic through MCP.
80
- - Flood odds/timelines when a summary answers the question.
1
+ export const SERVER_INSTRUCTIONS = `# Cito MCP — agent operating manual
2
+
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.
4
+
5
+ ## Hard rules
6
+
7
+ 1. **Never invent IDs.** Do not guess matchId, gameId, playerId, team slug, eventId, or boutId. Always obtain IDs from a prior tool result (resolve, live, schedule, search, or list). If the user gives a name ("T1", "s1mple", "UFC 300"), call resolve_entity/search_entities first.
8
+ 2. **Resolve before deep.** Name → ID/slug → summary/page → deep stats. Skip resolve only when the user already provided a Cito ID/slug.
9
+ 3. **One screen, one composite.** Prefer a page/summary tool over stitching 4–6 thin GETs.
10
+ 4. **Honor the envelope.** Parse ok, data, pagination, partial, error, meta.rateLimit, meta.entities. Partial section failures are not total failures — use successful sections.
11
+ 5. **Read-only.** All curated tools are safe to retry except where error.retryable is false for bad args / entitlement.
12
+
13
+ ## Game parameter
14
+
15
+ game enum (unless a tool documents otherwise): lol | cs2 | dota2 | ufc | cod | tennis | all
16
+
17
+ - Default when the user named one title: that game. Default for "what's live?" / multi-title: omit game or all on tools that accept it.
18
+ - On UNSUPPORTED_GAME / 403 for a title: stop retrying that game; call api_health; report plan gaps.
19
+ - Fortnite and other titles may appear via call_api/resources; do not assume they are in the curated enum until list_capabilities says so.
20
+
21
+ ## Preferred tool order
22
+
23
+ 1. Unsure → list_capabilities (filter by game or job). Optional: api_health for key/tier/included games.
24
+ 2. Name without ID → resolve_entity (best match) or search_entities (browse). Reuse returned id/slug.
25
+ 3. Live / upcoming → live_matches; upcoming_schedule.
26
+ 4. Match UI / recap → match_summary first. match_details only for timelines, full maps, demos, live state.
27
+ 5. Team page → team_profile. Player form → player_profile. Tables/ranks → standings.
28
+ 6. Pre-match → match_preview. Rivalry → head_to_head. Event / fight-night card → event_card.
29
+ 7. Escape hatch → call_api (allowlisted path prefixes; prefer GET). Prefer curated tools.
30
+
31
+ Mnemonic: resolve → live/schedule → summary → deep.
32
+
33
+ ## Parallel vs sequence
34
+
35
+ Parallel-safe: independent reads (team_profile A ∥ team_profile B; live_matches ∥ api_health). Cap ~3–5 concurrent agent tools; check meta.rateLimit.remaining.
36
+
37
+ Serial required: resolve → detail; pagination (cursor from page N only); live board → selected match deep-dive.
38
+
39
+ Prefer server-side fan-out inside composites (partial[] recovery) over agent N+1.
40
+
41
+ ## Pagination
42
+
43
+ Lists use pagination: { limit, offset?, total?, hasMore, nextCursor, prevCursor }. Default limit 20, max 50. Loop with cursor: pagination.nextCursor and the same filters. Never invent cursors. Empty list is ok:true with items:[].
44
+
45
+ ## Errors
46
+
47
+ ok:false → read error.code (VALIDATION, NOT_FOUND, UNSUPPORTED_GAME, RATE_LIMIT, UNAUTHORIZED, UPSTREAM, PATH_NOT_ALLOWED, NOT_IMPLEMENTED). Follow error.recover[]. Retry only when retryable.
48
+
49
+ ok:true with partial[] → use successful sections; do not treat partial as total fail.
50
+ ok:true with meta.warnings[] → filters or depth degraded; do not assume ignored filters applied.
51
+ resolve_entity ambiguity is soft: ok:true with data.needsDisambiguation and data.candidates — pick a candidate; do not wait for AMBIGUOUS_ENTITY.
52
+
53
+ ## Recipes
54
+
55
+ Live board: api_health (optional) → live_matches → match_summary for selected matchId.
56
+ UFC empty live (count=0): read section.note / emptyReason / health (workerAlive, lag) / supervisor / nextCard — do not claim "API offline" without workerAlive/lag; never invent matchups from supervisor shells.
57
+ Team page: resolve_entity {type:team} → team_profile.
58
+ Player card: resolve_entity {type:player|fighter} → player_profile.
59
+ Fight night / event card: resolve_entity {type:event} → event_card {includeMatches:true} → match_preview for a featured bout.
60
+ Match preview (named sides): resolve_entity each side (optional) → match_preview {teamA, teamB}.
61
+ App scaffold: list_capabilities ∥ api_health ∥ live_matches, then one composite per screen.
62
+
63
+ ## Resources
64
+
65
+ - cito://llms.txt — product context when available
66
+ - cito://capabilities — catalog summary
67
+ - cito://openapi.json — optional public OpenAPI fetch for typed clients
68
+
69
+ ## What not to do
70
+
71
+ - Invent match/player/team IDs from memory.
72
+ - Parallel-paginate the same list with different cursors.
73
+ - N+1 match_details for every live row.
74
+ - Retry UNSUPPORTED_GAME or VALIDATION unchanged.
75
+ - Ship production traffic through MCP.
76
+ - Flood odds/timelines when a summary answers the question.
81
77
  `;