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 +7 -7
- package/dist/client.js +39 -35
- package/dist/envelope.js +3 -13
- package/dist/http.js +0 -16
- package/dist/index.js +0 -39
- package/dist/install.js +0 -44
- package/dist/instructions.js +76 -80
- package/dist/scrub.js +111 -0
- package/dist/tools/cs2.js +1 -48
- package/dist/tools/index.js +0 -11
- package/dist/tools/insight.js +42 -174
- package/dist/tools/leaderboard.js +0 -21
- package/dist/tools/live.js +36 -127
- package/dist/tools/match.js +29 -184
- package/dist/tools/meta.js +7 -52
- package/dist/tools/normalize.js +6 -229
- package/dist/tools/odds.js +0 -66
- package/dist/tools/player.js +45 -179
- package/dist/tools/rankings.js +0 -38
- package/dist/tools/resolve.js +0 -136
- package/dist/tools/schedule.js +0 -55
- package/dist/tools/standings.js +21 -65
- package/dist/tools/team.js +2 -83
- package/dist/tools/tournaments.js +0 -37
- package/dist/tools/types.js +0 -17
- package/dist/version.js +0 -17
- package/package.json +22 -2
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
|
|
199
|
-
"bodyImageUrl": "https
|
|
200
|
-
"imageUrl": "https
|
|
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.**
|
|
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
|
|
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
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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));
|
package/dist/instructions.js
CHANGED
|
@@ -1,81 +1,77 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
##
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
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
|
`;
|