cito-mcp 0.2.6 → 0.2.7

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
@@ -38,33 +38,39 @@ Composites multi-fetch server-side, return a **stable JSON envelope**, and isola
38
38
 
39
39
  ## Install
40
40
 
41
- ### One-liner (published package)
41
+ ### One command (recommended)
42
42
 
43
43
  ```bash
44
- npx cito-mcp
44
+ npx cito-mcp install --key cito_your_key_here
45
45
  ```
46
46
 
47
- Requires `CITO_API_KEY` in the environment (see [Environment](#environment)). Most hosts inject env via their MCP config rather than a bare shell.
47
+ Detects the MCP clients installed on your machine Claude Code, Claude Desktop, Cursor, Windsurf, Codex CLI — and writes the config for each. Then restart your editor.
48
48
 
49
- ### Claude Code
49
+ It is safe to re-run: existing config is merged, not replaced, your other MCP servers are left alone, a `.cito-bak` backup is written before the first change, and running it again just rotates the key rather than adding a second entry.
50
50
 
51
51
  ```bash
52
- claude mcp add cito -e CITO_API_KEY=cito_your_key_here -- npx cito-mcp
52
+ npx cito-mcp install --dry-run # show the plan, write nothing
53
+ npx cito-mcp install --key … --client cursor # one client only
54
+ npx cito-mcp install --help
53
55
  ```
54
56
 
55
- Windows if `npx` fails under Claude:
57
+ Get a key at [citoapi.com/dashboard](https://citoapi.com/dashboard).
58
+
59
+ ### Manual — Claude Code
56
60
 
57
61
  ```bash
58
- claude mcp add cito -e CITO_API_KEY=cito_your_key_here -- cmd /c npx cito-mcp
62
+ claude mcp add cito -e CITO_API_KEY=cito_your_key_here "--" npx -y cito-mcp
59
63
  ```
60
64
 
65
+ **Quote the `--`.** PowerShell 5.1 strips a bare `--` before the CLI sees it; because `-e` takes a variable number of values it then swallows `npx -y cito-mcp` as env vars and fails with `unknown option '-y'`. The quoted form works in PowerShell, cmd, bash and zsh alike. `npx cito-mcp install` avoids the problem entirely.
66
+
61
67
  From a local clone (development):
62
68
 
63
69
  ```bash
64
70
  cd mcp
65
71
  npm install
66
72
  npm run build
67
- claude mcp add cito -e CITO_API_KEY=cito_your_key_here -- node "%CD%\dist\index.js"
73
+ claude mcp add cito -e CITO_API_KEY=cito_your_key_here "--" node "%CD%\dist\index.js"
68
74
  ```
69
75
 
70
76
  ### Cursor
@@ -460,13 +466,20 @@ Package: **`cito-mcp@0.2.4`**
460
466
  - `live_matches`
461
467
  - `resolve_entity` (e.g. T1 / s1mple)
462
468
  - `match_summary` with a real `matchId` from live/schedule
463
- 4. Confirm `package.json` version and README match the shipped tool list (15).
469
+ 4. Confirm `package.json` version and README match the shipped tool list.
464
470
  5. `npm publish` from `mcp/` (or your release pipeline) with appropriate npm auth / access.
471
+ `prepublishOnly` runs build + tests + smoke, so a broken build cannot ship.
465
472
 
466
473
  ### Install line for docs & marketing
467
474
 
468
475
  ```bash
469
- claude mcp add cito -e CITO_API_KEY=cito_… -- npx cito-mcp
476
+ npx cito-mcp install --key cito_…
477
+ ```
478
+
479
+ Manual fallback (note the quoted `--`, required for PowerShell):
480
+
481
+ ```bash
482
+ claude mcp add cito -e CITO_API_KEY=cito_… "--" npx -y cito-mcp
470
483
  ```
471
484
 
472
485
  ```json
package/dist/index.js CHANGED
@@ -19,6 +19,18 @@ import { SERVER_INSTRUCTIONS } from './instructions.js';
19
19
  import { allTools, getTool } from './tools/index.js';
20
20
  import { runTool } from './tools/types.js';
21
21
  import { PACKAGE_VERSION } from './version.js';
22
+ /**
23
+ * Subcommands run and exit before any transport exists.
24
+ *
25
+ * `install` writes editor config and prints to stdout, which would corrupt the
26
+ * JSON-RPC channel if it ran alongside the server — hence the early return.
27
+ * Handled here rather than in a second binary so the documented entry point
28
+ * stays `npx cito-mcp`.
29
+ */
30
+ if (process.argv[2] === 'install') {
31
+ const { runInstall } = await import('./install.js');
32
+ process.exit(await runInstall(process.argv.slice(3)));
33
+ }
22
34
  const API_KEY = process.env.CITO_API_KEY;
23
35
  /**
24
36
  * Tools that need no API key. These must keep working with the server
@@ -0,0 +1,340 @@
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
+ import { execFileSync } from 'node:child_process';
24
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs';
25
+ import { homedir, platform } from 'node:os';
26
+ import { dirname, join } from 'node:path';
27
+ import { PACKAGE_VERSION } from './version.js';
28
+ const SERVER_NAME = 'cito';
29
+ function envHome(name, fallback) {
30
+ const v = process.env[name];
31
+ return v && v.trim() ? v : fallback;
32
+ }
33
+ function clientsFor() {
34
+ const home = homedir();
35
+ const os = platform();
36
+ const appData = envHome('APPDATA', join(home, 'AppData', 'Roaming'));
37
+ const desktopConfig = os === 'win32'
38
+ ? join(appData, 'Claude', 'claude_desktop_config.json')
39
+ : os === 'darwin'
40
+ ? join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
41
+ : join(home, '.config', 'Claude', 'claude_desktop_config.json');
42
+ return [
43
+ {
44
+ id: 'claude-code',
45
+ label: 'Claude Code',
46
+ configPath: join(home, '.claude.json'),
47
+ detectPath: join(home, '.claude.json'),
48
+ format: 'json-mcpServers',
49
+ },
50
+ {
51
+ id: 'claude-desktop',
52
+ label: 'Claude Desktop',
53
+ configPath: desktopConfig,
54
+ detectPath: dirname(desktopConfig),
55
+ format: 'json-mcpServers',
56
+ },
57
+ {
58
+ id: 'cursor',
59
+ label: 'Cursor',
60
+ configPath: join(home, '.cursor', 'mcp.json'),
61
+ detectPath: join(home, '.cursor'),
62
+ format: 'json-mcpServers',
63
+ },
64
+ {
65
+ id: 'windsurf',
66
+ label: 'Windsurf',
67
+ configPath: join(home, '.codeium', 'windsurf', 'mcp_config.json'),
68
+ detectPath: join(home, '.codeium', 'windsurf'),
69
+ format: 'json-mcpServers',
70
+ },
71
+ {
72
+ id: 'codex',
73
+ label: 'Codex CLI',
74
+ configPath: join(home, '.codex', 'config.toml'),
75
+ detectPath: join(home, '.codex'),
76
+ format: 'toml-mcp_servers',
77
+ },
78
+ ];
79
+ }
80
+ export function maskKey(key) {
81
+ if (key.length <= 12)
82
+ return '****';
83
+ return `${key.slice(0, 9)}…${key.slice(-4)}`;
84
+ }
85
+ /** The stdio entry every JSON-based client understands. */
86
+ function serverEntry(key, base) {
87
+ return {
88
+ type: 'stdio',
89
+ command: 'npx',
90
+ args: ['-y', 'cito-mcp'],
91
+ env: { CITO_API_KEY: key, ...(base ? { CITO_API_BASE: base } : {}) },
92
+ };
93
+ }
94
+ function readJson(path) {
95
+ if (!existsSync(path))
96
+ return null;
97
+ try {
98
+ const raw = readFileSync(path, 'utf8').trim();
99
+ if (!raw)
100
+ return {};
101
+ const parsed = JSON.parse(raw);
102
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ /**
109
+ * Escape a TOML basic string. Only the characters TOML actually requires —
110
+ * an API key with a quote or backslash would otherwise produce a file the
111
+ * client cannot parse.
112
+ */
113
+ function tomlString(v) {
114
+ return `"${v.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
115
+ }
116
+ function tomlBlock(key, base) {
117
+ const envLines = [`CITO_API_KEY = ${tomlString(key)}`]
118
+ .concat(base ? [`CITO_API_BASE = ${tomlString(base)}`] : [])
119
+ .join('\n');
120
+ return [
121
+ `[mcp_servers.${SERVER_NAME}]`,
122
+ `command = "npx"`,
123
+ `args = ["-y", "cito-mcp"]`,
124
+ ``,
125
+ `[mcp_servers.${SERVER_NAME}.env]`,
126
+ envLines,
127
+ ``,
128
+ ].join('\n');
129
+ }
130
+ /**
131
+ * Replace an existing [mcp_servers.cito] block (and its .env subtable) or
132
+ * append a new one. Deliberately conservative: it only recognises the exact
133
+ * headers it writes, so a hand-edited file is appended to rather than mangled.
134
+ */
135
+ export function upsertTomlBlock(existing, key, base) {
136
+ const block = tomlBlock(key, base);
137
+ const header = new RegExp(`^\\[mcp_servers\\.${SERVER_NAME}(\\.[A-Za-z_]+)?\\]\\s*$`);
138
+ const anyHeader = /^\[[^\]]+\]\s*$/;
139
+ const lines = existing.split(/\r?\n/);
140
+ const kept = [];
141
+ let skipping = false;
142
+ for (const line of lines) {
143
+ if (header.test(line)) {
144
+ skipping = true;
145
+ continue;
146
+ }
147
+ if (skipping && anyHeader.test(line))
148
+ skipping = false;
149
+ if (!skipping)
150
+ kept.push(line);
151
+ }
152
+ const body = kept.join('\n').replace(/\n{3,}$/, '\n\n').replace(/\s*$/, '');
153
+ return (body ? `${body}\n\n` : '') + block;
154
+ }
155
+ /**
156
+ * Claude Code owns the schema of ~/.claude.json and stores far more than MCP
157
+ * config in it, so let its own CLI do the write when it is on PATH. Direct
158
+ * editing is the fallback, not the default.
159
+ */
160
+ function tryClaudeCli(key, base, dryRun) {
161
+ const json = JSON.stringify(serverEntry(key, base));
162
+ // On Windows `claude` is a .cmd shim, which execFileSync cannot spawn
163
+ // directly — it needs a shell. Quoting matters there because the JSON
164
+ // payload contains spaces and double quotes.
165
+ const isWin = platform() === 'win32';
166
+ const run = (args) => {
167
+ if (!isWin) {
168
+ execFileSync('claude', args, { stdio: 'ignore' });
169
+ return;
170
+ }
171
+ execFileSync('cmd', ['/c', 'claude', ...args], { stdio: 'ignore' });
172
+ };
173
+ try {
174
+ if (dryRun) {
175
+ run(['--version']);
176
+ return 'would use `claude mcp add-json` (CLI detected)';
177
+ }
178
+ run(['mcp', 'add-json', SERVER_NAME, json, '-s', 'user']);
179
+ return 'via `claude mcp add-json` (user scope)';
180
+ }
181
+ catch {
182
+ return null;
183
+ }
184
+ }
185
+ function writeJsonClient(c, key, base, dryRun) {
186
+ const current = readJson(c.configPath);
187
+ if (current === null && existsSync(c.configPath)) {
188
+ return {
189
+ client: c,
190
+ status: 'failed',
191
+ note: `${c.configPath} is not valid JSON — left untouched`,
192
+ };
193
+ }
194
+ const config = current ?? {};
195
+ const servers = (config.mcpServers ?? {});
196
+ const existed = Object.prototype.hasOwnProperty.call(servers, SERVER_NAME);
197
+ const next = { ...config, mcpServers: { ...servers, [SERVER_NAME]: serverEntry(key, base) } };
198
+ if (dryRun) {
199
+ return {
200
+ client: c,
201
+ status: 'planned',
202
+ note: `${existed ? 'replace' : 'add'} "${SERVER_NAME}" in ${c.configPath}`,
203
+ };
204
+ }
205
+ mkdirSync(dirname(c.configPath), { recursive: true });
206
+ if (existsSync(c.configPath))
207
+ copyFileSync(c.configPath, `${c.configPath}.cito-bak`);
208
+ writeFileSync(c.configPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
209
+ return {
210
+ client: c,
211
+ status: 'written',
212
+ note: `${existed ? 'updated' : 'added'} in ${c.configPath}`,
213
+ };
214
+ }
215
+ function writeTomlClient(c, key, base, dryRun) {
216
+ const existing = existsSync(c.configPath) ? readFileSync(c.configPath, 'utf8') : '';
217
+ const existed = new RegExp(`\\[mcp_servers\\.${SERVER_NAME}\\]`).test(existing);
218
+ if (dryRun) {
219
+ return {
220
+ client: c,
221
+ status: 'planned',
222
+ note: `${existed ? 'replace' : 'add'} [mcp_servers.${SERVER_NAME}] in ${c.configPath}`,
223
+ };
224
+ }
225
+ mkdirSync(dirname(c.configPath), { recursive: true });
226
+ if (existsSync(c.configPath))
227
+ copyFileSync(c.configPath, `${c.configPath}.cito-bak`);
228
+ writeFileSync(c.configPath, upsertTomlBlock(existing, key, base), 'utf8');
229
+ return {
230
+ client: c,
231
+ status: 'written',
232
+ note: `${existed ? 'updated' : 'added'} in ${c.configPath}`,
233
+ };
234
+ }
235
+ function parseArgs(argv) {
236
+ const out = { key: undefined, base: undefined, only: [], dryRun: false, help: false };
237
+ for (let i = 0; i < argv.length; i++) {
238
+ const a = argv[i];
239
+ const eat = () => argv[++i];
240
+ if (a === '--help' || a === '-h')
241
+ out.help = true;
242
+ else if (a === '--dry-run' || a === '-n')
243
+ out.dryRun = true;
244
+ else if (a === '--key')
245
+ out.key = eat();
246
+ else if (a?.startsWith('--key='))
247
+ out.key = a.slice(6);
248
+ else if (a === '--base')
249
+ out.base = eat();
250
+ else if (a?.startsWith('--base='))
251
+ out.base = a.slice(7);
252
+ else if (a === '--client')
253
+ out.only.push(...(eat() ?? '').split(',').filter(Boolean));
254
+ else if (a?.startsWith('--client='))
255
+ out.only.push(...a.slice(9).split(',').filter(Boolean));
256
+ }
257
+ return out;
258
+ }
259
+ const HELP = `cito-mcp install — configure the Cito MCP server for your editors
260
+
261
+ Usage:
262
+ npx cito-mcp install --key cito_xxx
263
+ npx cito-mcp install --key cito_xxx --client cursor,claude-code
264
+ npx cito-mcp install --dry-run
265
+
266
+ Options:
267
+ --key <key> Cito API key. Falls back to $CITO_API_KEY.
268
+ --client <ids> Comma-separated: claude-code, claude-desktop, cursor, windsurf, codex.
269
+ Default: every client detected on this machine.
270
+ --base <url> Override API base (staging / self-hosted).
271
+ --dry-run, -n Show what would change; write nothing.
272
+ --help, -h This message.
273
+
274
+ Get a key at https://citoapi.com/dashboard`;
275
+ export async function runInstall(argv) {
276
+ const args = parseArgs(argv);
277
+ if (args.help) {
278
+ console.log(HELP);
279
+ return 0;
280
+ }
281
+ const key = args.key ?? process.env.CITO_API_KEY;
282
+ if (!key && !args.dryRun) {
283
+ console.error('No API key. Pass --key cito_xxx or set CITO_API_KEY.\n');
284
+ console.error('Get one at https://citoapi.com/dashboard');
285
+ return 1;
286
+ }
287
+ const all = clientsFor();
288
+ const unknown = args.only.filter((id) => !all.some((c) => c.id === id));
289
+ if (unknown.length) {
290
+ console.error(`Unknown client(s): ${unknown.join(', ')}`);
291
+ console.error(`Valid: ${all.map((c) => c.id).join(', ')}`);
292
+ return 1;
293
+ }
294
+ // An explicit --client is a instruction, not a guess: honour it even if we
295
+ // cannot detect the client (fresh install, portable install, unusual path).
296
+ const selected = args.only.length
297
+ ? all.filter((c) => args.only.includes(c.id))
298
+ : all.filter((c) => existsSync(c.detectPath));
299
+ if (selected.length === 0) {
300
+ console.error('No supported MCP clients detected.');
301
+ console.error(`Looked for: ${all.map((c) => c.label).join(', ')}`);
302
+ console.error('Use --client <id> to configure one anyway.');
303
+ return 1;
304
+ }
305
+ console.log(`cito-mcp ${PACKAGE_VERSION} installer`);
306
+ console.log(key ? `key: ${maskKey(key)}` : 'key: (none — dry run)');
307
+ console.log(args.dryRun ? 'mode: dry run, nothing will be written\n' : '');
308
+ const effectiveKey = key ?? 'CITO_API_KEY';
309
+ const results = [];
310
+ for (const c of selected) {
311
+ if (c.id === 'claude-code') {
312
+ const viaCli = tryClaudeCli(effectiveKey, args.base, args.dryRun);
313
+ if (viaCli) {
314
+ results.push({ client: c, status: args.dryRun ? 'planned' : 'written', note: viaCli });
315
+ continue;
316
+ }
317
+ }
318
+ results.push(c.format === 'toml-mcp_servers'
319
+ ? writeTomlClient(c, effectiveKey, args.base, args.dryRun)
320
+ : writeJsonClient(c, effectiveKey, args.base, args.dryRun));
321
+ }
322
+ for (const r of results) {
323
+ const mark = r.status === 'failed' ? '✗' : r.status === 'skipped' ? '–' : '✓';
324
+ console.log(` ${mark} ${r.client.label.padEnd(16)} ${r.note}`);
325
+ }
326
+ const failed = results.filter((r) => r.status === 'failed');
327
+ console.log('');
328
+ if (args.dryRun) {
329
+ console.log('Dry run complete. Re-run without --dry-run to apply.');
330
+ }
331
+ else {
332
+ console.log('Restart your editor to load the server, then ask it:');
333
+ console.log(' "what UFC events are coming up?"');
334
+ }
335
+ if (failed.length) {
336
+ console.log(`\n${failed.length} client(s) could not be configured (see above).`);
337
+ return 1;
338
+ }
339
+ return 0;
340
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cito-mcp",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Standalone MCP server for the Cito esports API — 15 curated outcome tools for agents (live, schedule, profiles, standings, previews, event cards).",
5
5
  "type": "module",
6
6
  "bin": {