cito-mcp 0.4.3 → 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 +6 -7
- package/dist/client.js +0 -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 +0 -4
- package/dist/scrub.js +67 -0
- package/dist/tools/cs2.js +1 -48
- package/dist/tools/index.js +0 -6
- package/dist/tools/insight.js +4 -136
- package/dist/tools/leaderboard.js +0 -21
- package/dist/tools/live.js +4 -95
- package/dist/tools/match.js +94 -123
- package/dist/tools/meta.js +7 -62
- package/dist/tools/normalize.js +6 -229
- package/dist/tools/odds.js +0 -66
- package/dist/tools/player.js +1 -135
- 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 +1 -45
- package/dist/tools/team.js +39 -120
- package/dist/tools/tournaments.js +0 -37
- package/dist/tools/types.js +0 -17
- package/dist/version.js +0 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,6 @@ v0.1 exposed ~100+ tools auto-generated from OpenAPI. Agents had to pick among t
|
|
|
28
28
|
| Pre-match briefing (with surface) | `match_preview` |
|
|
29
29
|
| Event / fight-night card | `event_card` |
|
|
30
30
|
| Rivalry record | `head_to_head` |
|
|
31
|
-
| Tennis betting odds | `tennis_odds` |
|
|
32
31
|
| Tennis tournament catalog | `tournaments` |
|
|
33
32
|
| One day of tennis | `tennis_schedule` |
|
|
34
33
|
| Tennis player match log | `player_matches` |
|
|
@@ -196,15 +195,15 @@ on `event_card` bout corners and on `player_profile`. No extra call, no N+1.
|
|
|
196
195
|
"record": { "wins": 28, "losses": 1, "draws": 0, "text": "28-1-0 (W-L-D)" },
|
|
197
196
|
"championStatus": "champion",
|
|
198
197
|
"images": {
|
|
199
|
-
"headshotUrl": "https
|
|
200
|
-
"bodyImageUrl": "https
|
|
201
|
-
"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/…",
|
|
202
201
|
"proxiedImageUrl": "https://api.citoapi.com/api/v1/public/images/ufc/aHR0cHM6…"
|
|
203
202
|
}
|
|
204
203
|
}
|
|
205
204
|
```
|
|
206
205
|
|
|
207
|
-
**Use `proxiedImageUrl` in a browser.**
|
|
206
|
+
**Use `proxiedImageUrl` in a browser.** the image host sends no CORS header and can
|
|
208
207
|
hotlink-block, so raw URLs render as broken images in a web UI. The proxied URL
|
|
209
208
|
is served by the API and is safe to put in an `<img src>`.
|
|
210
209
|
|
|
@@ -214,7 +213,7 @@ never partially shaped — if any image exists, all four keys are present.
|
|
|
214
213
|
|
|
215
214
|
Tennis is the exception on purpose: the feed supplies a single `portrait_url`
|
|
216
215
|
(Wikimedia Commons) and no square crop, so that one URL fills both `headshotUrl`
|
|
217
|
-
and `imageUrl`, and `proxiedImageUrl` stays `null` because only
|
|
216
|
+
and `imageUrl`, and `proxiedImageUrl` stays `null` because only fighter image URLs have
|
|
218
217
|
a proxy.
|
|
219
218
|
|
|
220
219
|
**Tennis images carry their licence and credit** — and a UI is expected to render
|
|
@@ -561,7 +560,7 @@ Second tennis correctness pass, from the 2026-09-12 re-sweep. Five defects, each
|
|
|
561
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.
|
|
562
561
|
|
|
563
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`.
|
|
564
|
-
- **`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`.
|
|
565
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.
|
|
566
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.
|
|
567
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));
|
package/dist/instructions.js
CHANGED
|
@@ -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. "
|
|
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,
|
package/dist/tools/index.js
CHANGED
|
@@ -9,10 +9,8 @@ import { rankingsTools } from './rankings.js';
|
|
|
9
9
|
import { leaderboardTools } from './leaderboard.js';
|
|
10
10
|
import { insightTools } from './insight.js';
|
|
11
11
|
import { cs2Tools } from './cs2.js';
|
|
12
|
-
import { oddsTools } from './odds.js';
|
|
13
12
|
import { tournamentTools } from './tournaments.js';
|
|
14
13
|
import { scheduleTools } from './schedule.js';
|
|
15
|
-
/** Curated outcome-tool catalog. Order matches preferred cold-start ladder. */
|
|
16
14
|
export const allTools = [
|
|
17
15
|
...metaTools.filter((t) => t.name === 'list_capabilities' || t.name === 'api_health'),
|
|
18
16
|
...resolveTools,
|
|
@@ -23,14 +21,10 @@ export const allTools = [
|
|
|
23
21
|
...standingsTools,
|
|
24
22
|
...rankingsTools,
|
|
25
23
|
...leaderboardTools,
|
|
26
|
-
// Tennis depth added after the coverage audit: odds, tournament discovery and
|
|
27
|
-
// the daily schedule were reachable only through call_api.
|
|
28
|
-
...oddsTools,
|
|
29
24
|
...tournamentTools,
|
|
30
25
|
...scheduleTools,
|
|
31
26
|
...insightTools,
|
|
32
27
|
...cs2Tools,
|
|
33
|
-
// Escape-hatch pair last: discover routes, then call one.
|
|
34
28
|
...metaTools.filter((t) => t.name === 'list_routes' || t.name === 'call_api'),
|
|
35
29
|
];
|
|
36
30
|
export function getTool(name) {
|