cito-mcp 0.1.0 → 0.2.2
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 +396 -44
- package/dist/client.js +208 -0
- package/dist/envelope.js +210 -0
- package/dist/index.js +103 -201
- package/dist/instructions.js +80 -0
- package/dist/tools/index.js +23 -0
- package/dist/tools/insight.js +1013 -0
- package/dist/tools/live.js +447 -0
- package/dist/tools/match.js +464 -0
- package/dist/tools/meta.js +609 -0
- package/dist/tools/normalize.js +177 -0
- package/dist/tools/player.js +357 -0
- package/dist/tools/resolve.js +518 -0
- package/dist/tools/standings.js +319 -0
- package/dist/tools/team.js +704 -0
- package/dist/tools/types.js +85 -0
- package/package.json +3 -3
- package/dist/executor.js +0 -75
- package/dist/spec.js +0 -193
- package/dist/tools.js +0 -203
package/dist/index.js
CHANGED
|
@@ -1,196 +1,84 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* cito-mcp —
|
|
3
|
+
* cito-mcp — curated MCP server for the Cito esports API.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* default (Claude Code / Cursor); `--http <port>` serves a stateless
|
|
8
|
-
* Streamable HTTP endpoint instead.
|
|
5
|
+
* 14 outcome tools (not OpenAPI mass-generation). stdio by default;
|
|
6
|
+
* `--http <port>` serves a stateless Streamable HTTP endpoint.
|
|
9
7
|
*
|
|
10
8
|
* Required env: CITO_API_KEY (never logged).
|
|
11
|
-
* Optional env: CITO_API_BASE
|
|
9
|
+
* Optional env: CITO_API_BASE (default https://api.citoapi.com/api/v1).
|
|
12
10
|
*/
|
|
13
11
|
import { createServer as createHttpServer } from 'node:http';
|
|
14
|
-
import { fileURLToPath } from 'node:url';
|
|
15
|
-
import { dirname, join } from 'node:path';
|
|
16
12
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
17
13
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
18
14
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
19
15
|
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
20
|
-
import { DEFAULT_API_BASE,
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
16
|
+
import { DEFAULT_API_BASE, log } from './client.js';
|
|
17
|
+
import { errorEnvelope, toMcpResult } from './envelope.js';
|
|
18
|
+
import { SERVER_INSTRUCTIONS } from './instructions.js';
|
|
19
|
+
import { allTools, getTool } from './tools/index.js';
|
|
20
|
+
import { runTool } from './tools/types.js';
|
|
21
|
+
const PACKAGE_VERSION = '0.2.2';
|
|
23
22
|
const API_KEY = process.env.CITO_API_KEY;
|
|
24
23
|
if (!API_KEY) {
|
|
25
24
|
console.error('[cito-mcp] CITO_API_KEY is required.\n' +
|
|
26
25
|
'Set it to your Cito API key, e.g.:\n' +
|
|
27
|
-
' claude mcp add cito
|
|
26
|
+
' claude mcp add cito -e CITO_API_KEY=cito_... -- npx cito-mcp');
|
|
28
27
|
process.exit(1);
|
|
29
28
|
}
|
|
30
29
|
const API_BASE = (process.env.CITO_API_BASE || DEFAULT_API_BASE).replace(/\/+$/, '');
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return { content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) };
|
|
36
|
-
}
|
|
37
|
-
async function fetchJson(url) {
|
|
38
|
-
const response = await fetch(url, {
|
|
39
|
-
headers: { 'x-api-key': API_KEY, accept: 'application/json' },
|
|
40
|
-
});
|
|
41
|
-
const text = await response.text();
|
|
42
|
-
let data = null;
|
|
43
|
-
try {
|
|
44
|
-
data = JSON.parse(text);
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
data = text;
|
|
48
|
-
}
|
|
49
|
-
return { ok: response.ok, status: response.status, data };
|
|
50
|
-
}
|
|
51
|
-
/** Curated: one merged "what's live right now" answer across all games. */
|
|
52
|
-
async function liveOverview() {
|
|
53
|
-
const games = [
|
|
54
|
-
{ game: 'lol', path: '/lol/live', label: (m) => `${m.league ?? ''} ${m.team1 ?? m.teams?.[0] ?? '?'} vs ${m.team2 ?? m.teams?.[1] ?? '?'}` },
|
|
55
|
-
{ game: 'dota2', path: '/dota2/matches/live', label: (m) => `${m.team1 ?? m.team1Name ?? '?'} vs ${m.team2 ?? m.team2Name ?? '?'}` },
|
|
56
|
-
{ game: 'cs2', path: '/cs2/matches/live', label: (m) => `${m.team1 ?? m.team1Name ?? '?'} vs ${m.team2 ?? m.team2Name ?? '?'}` },
|
|
57
|
-
{ game: 'cod', path: '/cod/matches/live', label: (m) => `${m.team1 ?? m.team1Name ?? '?'} vs ${m.team2 ?? m.team2Name ?? '?'}` },
|
|
58
|
-
];
|
|
59
|
-
const sections = await Promise.all(games.map(async ({ game, path, label }) => {
|
|
60
|
-
const { ok, status, data } = await fetchJson(`${API_BASE}${path}`);
|
|
61
|
-
if (!ok)
|
|
62
|
-
return { game, count: null, note: `unavailable (HTTP ${status})`, matches: [] };
|
|
63
|
-
const rows = Array.isArray(data) ? data : Array.isArray(data?.data) ? data.data : [];
|
|
64
|
-
return {
|
|
65
|
-
game,
|
|
66
|
-
count: rows.length,
|
|
67
|
-
note: null,
|
|
68
|
-
matches: rows.slice(0, 5).map((row) => label(row).replace(/\s+/g, ' ').trim()),
|
|
69
|
-
};
|
|
70
|
-
}));
|
|
71
|
-
return textResult(JSON.stringify({ liveNow: sections }, null, 2));
|
|
72
|
-
}
|
|
73
|
-
/** Curated: /health plus a fallback authed probe to prove the key works. */
|
|
74
|
-
async function apiHealth() {
|
|
75
|
-
async function probe(path) {
|
|
76
|
-
try {
|
|
77
|
-
const response = await fetch(`${API_BASE}${path}`, {
|
|
78
|
-
headers: { 'x-api-key': API_KEY, accept: 'application/json' },
|
|
79
|
-
});
|
|
80
|
-
return { ok: response.ok, status: response.status, body: await response.text(), headers: response.headers };
|
|
81
|
-
}
|
|
82
|
-
catch (error) {
|
|
83
|
-
return { ok: false, status: 0, body: error.message };
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
let answeredBy = 'health';
|
|
87
|
-
let result = await probe('/health');
|
|
88
|
-
if (!result.ok) {
|
|
89
|
-
const fallback = await probe('/lol/leagues');
|
|
90
|
-
if (fallback.ok) {
|
|
91
|
-
answeredBy = 'lol/leagues';
|
|
92
|
-
result = fallback;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return textResult(JSON.stringify({
|
|
96
|
-
probe: {
|
|
97
|
-
answeredBy,
|
|
98
|
-
ok: result.ok,
|
|
99
|
-
status: result.status,
|
|
100
|
-
body: present(result.body, 4096),
|
|
101
|
-
},
|
|
102
|
-
apiKey: {
|
|
103
|
-
valid: result.ok,
|
|
104
|
-
...(result.ok ? {} : { hint: 'check CITO_API_KEY' }),
|
|
105
|
-
},
|
|
106
|
-
rateLimit: result.headers
|
|
107
|
-
? {
|
|
108
|
-
tier: result.headers.get('x-cito-tier') ?? undefined,
|
|
109
|
-
limit: result.headers.get('x-ratelimit-limit') ?? undefined,
|
|
110
|
-
remaining: result.headers.get('x-ratelimit-remaining') ?? undefined,
|
|
111
|
-
}
|
|
112
|
-
: undefined,
|
|
113
|
-
}, null, 2));
|
|
114
|
-
}
|
|
30
|
+
const ctx = {
|
|
31
|
+
apiKey: API_KEY,
|
|
32
|
+
baseUrl: API_BASE,
|
|
33
|
+
};
|
|
115
34
|
async function main() {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
apiKey: API_KEY,
|
|
124
|
-
opportunisticKeys: OPPORTUNISTIC_SPEC_KEYS,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
catch (error) {
|
|
128
|
-
console.error(`[cito-mcp] could not load any OpenAPI spec (no cache at ${CACHE_PATH}): ${error.message}`);
|
|
129
|
-
process.exit(1);
|
|
130
|
-
}
|
|
131
|
-
if (state.sources.length === 0) {
|
|
132
|
-
console.error(`[cito-mcp] no OpenAPI specs available (all skipped, no cache at ${CACHE_PATH})`);
|
|
133
|
-
process.exit(1);
|
|
134
|
-
}
|
|
135
|
-
for (const source of state.sources) {
|
|
136
|
-
log(`spec '${source.key}' loaded from ${source.origin} (${Object.keys(source.spec.paths).length} paths, base ${source.baseUrl})`);
|
|
137
|
-
}
|
|
138
|
-
log(`refreshing specs every ${REFRESH_MINUTES}min`);
|
|
139
|
-
let tools = new Map(generateAllTools(state.sources).map((tool) => [tool.name, tool]));
|
|
140
|
-
log(`generated ${tools.size} tools from ${state.sources.length} spec source(s)`);
|
|
141
|
-
const server = new Server({ name: 'cito-mcp', version: PACKAGE_VERSION }, { capabilities: { tools: {}, resources: {} } });
|
|
142
|
-
const CURATED_TOOLS = [
|
|
143
|
-
{
|
|
144
|
-
name: 'cito_live_overview',
|
|
145
|
-
description: 'Merged "what is live right now" across all games (LoL, Dota 2, CS2, COD): per-game live match count and the first few match labels. Use this before diving into per-game live endpoints.',
|
|
146
|
-
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
147
|
-
handler: liveOverview,
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
name: 'cito_api_health',
|
|
151
|
-
description: 'API gateway health plus an authenticated probe proving CITO_API_KEY works (returns key validity and rate-limit/tier headers when present).',
|
|
152
|
-
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
153
|
-
handler: apiHealth,
|
|
154
|
-
},
|
|
155
|
-
];
|
|
35
|
+
const server = new Server({
|
|
36
|
+
name: 'cito-mcp',
|
|
37
|
+
version: PACKAGE_VERSION,
|
|
38
|
+
}, {
|
|
39
|
+
capabilities: { tools: {}, resources: {} },
|
|
40
|
+
instructions: SERVER_INSTRUCTIONS,
|
|
41
|
+
});
|
|
156
42
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
157
|
-
tools:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
})),
|
|
163
|
-
...CURATED_TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })),
|
|
164
|
-
],
|
|
43
|
+
tools: allTools.map(({ name, description, inputSchema }) => ({
|
|
44
|
+
name,
|
|
45
|
+
description,
|
|
46
|
+
inputSchema,
|
|
47
|
+
})),
|
|
165
48
|
}));
|
|
166
49
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
167
50
|
const { name } = request.params;
|
|
168
51
|
const args = (request.params.arguments ?? {});
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
171
|
-
return
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
catch (error) {
|
|
179
|
-
return textResult(`request failed: ${error.message}`, true);
|
|
52
|
+
const tool = getTool(name);
|
|
53
|
+
if (!tool) {
|
|
54
|
+
return toMcpResult(errorEnvelope({
|
|
55
|
+
code: 'VALIDATION',
|
|
56
|
+
message: `Unknown tool: ${name}`,
|
|
57
|
+
game: null,
|
|
58
|
+
source: 'cito-mcp',
|
|
59
|
+
recover: ['Call list_capabilities to see available tools'],
|
|
60
|
+
}));
|
|
180
61
|
}
|
|
62
|
+
return runTool(tool, args, ctx);
|
|
181
63
|
});
|
|
182
64
|
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
183
65
|
resources: [
|
|
184
66
|
{
|
|
185
67
|
uri: 'cito://llms.txt',
|
|
186
68
|
name: 'Cito API agent context (llms.txt)',
|
|
187
|
-
description: 'The Cito API llms.txt file — curated context about the API for AI agents.',
|
|
69
|
+
description: 'The Cito API llms.txt file — curated context about the API for AI agents (fetched on read).',
|
|
188
70
|
mimeType: 'text/plain',
|
|
189
71
|
},
|
|
72
|
+
{
|
|
73
|
+
uri: 'cito://capabilities',
|
|
74
|
+
name: 'cito-mcp capabilities summary',
|
|
75
|
+
description: 'Static summary of curated tools, games, and preferred workflow.',
|
|
76
|
+
mimeType: 'application/json',
|
|
77
|
+
},
|
|
190
78
|
{
|
|
191
79
|
uri: 'cito://openapi.json',
|
|
192
|
-
name: 'Cito OpenAPI
|
|
193
|
-
description: '
|
|
80
|
+
name: 'Cito OpenAPI (public fetch)',
|
|
81
|
+
description: 'Live public OpenAPI snapshot for typed clients (not the tool catalog source).',
|
|
194
82
|
mimeType: 'application/json',
|
|
195
83
|
},
|
|
196
84
|
],
|
|
@@ -198,56 +86,71 @@ async function main() {
|
|
|
198
86
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
199
87
|
const { uri } = request.params;
|
|
200
88
|
if (uri === 'cito://llms.txt') {
|
|
201
|
-
const origin = new URL(API_BASE).origin;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
89
|
+
const origin = new URL(API_BASE.endsWith('/api/v1') ? API_BASE.slice(0, -'/api/v1'.length) || API_BASE : API_BASE).origin;
|
|
90
|
+
try {
|
|
91
|
+
const response = await fetch(`${origin}/llms.txt`);
|
|
92
|
+
return {
|
|
93
|
+
contents: [{ uri, mimeType: 'text/plain', text: await response.text() }],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
return {
|
|
98
|
+
contents: [
|
|
99
|
+
{
|
|
100
|
+
uri,
|
|
101
|
+
mimeType: 'text/plain',
|
|
102
|
+
text: `llms.txt unavailable: ${error.message}`,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
206
107
|
}
|
|
207
|
-
if (uri === 'cito://
|
|
108
|
+
if (uri === 'cito://capabilities') {
|
|
208
109
|
return {
|
|
209
|
-
contents: [
|
|
110
|
+
contents: [
|
|
111
|
+
{
|
|
210
112
|
uri,
|
|
211
113
|
mimeType: 'application/json',
|
|
212
114
|
text: JSON.stringify({
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
115
|
+
version: PACKAGE_VERSION,
|
|
116
|
+
tools: allTools.map((t) => t.name),
|
|
117
|
+
games: ['lol', 'cs2', 'dota2', 'cod', 'ufc'],
|
|
118
|
+
workflow: [
|
|
119
|
+
'list_capabilities / api_health',
|
|
120
|
+
'resolve_entity → profiles / match tools',
|
|
121
|
+
'live_matches / upcoming_schedule',
|
|
122
|
+
'match_summary → match_details',
|
|
123
|
+
'call_api escape hatch only',
|
|
124
|
+
],
|
|
221
125
|
}, null, 2),
|
|
222
|
-
}
|
|
126
|
+
},
|
|
127
|
+
],
|
|
223
128
|
};
|
|
224
129
|
}
|
|
130
|
+
if (uri === 'cito://openapi.json') {
|
|
131
|
+
try {
|
|
132
|
+
const response = await fetch(`${API_BASE}/openapi.json`, {
|
|
133
|
+
headers: { 'x-api-key': API_KEY, accept: 'application/json' },
|
|
134
|
+
});
|
|
135
|
+
const text = await response.text();
|
|
136
|
+
return {
|
|
137
|
+
contents: [{ uri, mimeType: 'application/json', text }],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
return {
|
|
142
|
+
contents: [
|
|
143
|
+
{
|
|
144
|
+
uri,
|
|
145
|
+
mimeType: 'application/json',
|
|
146
|
+
text: JSON.stringify({ error: error.message }),
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
225
152
|
throw new Error(`unknown resource: ${uri}`);
|
|
226
153
|
});
|
|
227
|
-
const refreshTimer = setInterval(() => {
|
|
228
|
-
void (async () => {
|
|
229
|
-
const next = await refreshSpecSourcesIfDue(state, {
|
|
230
|
-
defs,
|
|
231
|
-
apiBase: API_BASE,
|
|
232
|
-
cachePath: CACHE_PATH,
|
|
233
|
-
apiKey: API_KEY,
|
|
234
|
-
refreshMinutes: REFRESH_MINUTES,
|
|
235
|
-
opportunisticKeys: OPPORTUNISTIC_SPEC_KEYS,
|
|
236
|
-
});
|
|
237
|
-
if (next) {
|
|
238
|
-
state = next;
|
|
239
|
-
tools = new Map(generateAllTools(state.sources).map((tool) => [tool.name, tool]));
|
|
240
|
-
log(`tools regenerated (${tools.size}) after spec refresh`);
|
|
241
|
-
try {
|
|
242
|
-
void server.sendToolListChanged();
|
|
243
|
-
}
|
|
244
|
-
catch {
|
|
245
|
-
// Client may not support list_changed — it will re-list on next use.
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
})();
|
|
249
|
-
}, 60_000);
|
|
250
|
-
refreshTimer.unref();
|
|
251
154
|
const httpPort = (() => {
|
|
252
155
|
const index = process.argv.indexOf('--http');
|
|
253
156
|
if (index === -1)
|
|
@@ -256,7 +159,6 @@ async function main() {
|
|
|
256
159
|
return Number.isInteger(port) && port > 0 ? port : null;
|
|
257
160
|
})();
|
|
258
161
|
if (httpPort) {
|
|
259
|
-
// Stateless Streamable HTTP: one transport, no session tracking.
|
|
260
162
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
261
163
|
await server.connect(transport);
|
|
262
164
|
const httpServer = createHttpServer(async (req, res) => {
|
|
@@ -280,12 +182,12 @@ async function main() {
|
|
|
280
182
|
await transport.handleRequest(req, res, body);
|
|
281
183
|
});
|
|
282
184
|
httpServer.listen(httpPort, () => {
|
|
283
|
-
log(`cito-mcp listening on http://127.0.0.1:${httpPort}/mcp (${
|
|
185
|
+
log(`cito-mcp ${PACKAGE_VERSION} listening on http://127.0.0.1:${httpPort}/mcp (${allTools.length} curated tools)`);
|
|
284
186
|
});
|
|
285
187
|
}
|
|
286
188
|
else {
|
|
287
189
|
await server.connect(new StdioServerTransport());
|
|
288
|
-
log(`cito-mcp
|
|
190
|
+
log(`cito-mcp ${PACKAGE_VERSION} on stdio (${allTools.length} curated tools)`);
|
|
289
191
|
}
|
|
290
192
|
}
|
|
291
193
|
await main();
|
|
@@ -0,0 +1,80 @@
|
|
|
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 | 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
|
+
Team page: resolve_entity {type:team} → team_profile.
|
|
61
|
+
Player card: resolve_entity {type:player|fighter} → player_profile.
|
|
62
|
+
Fight night / event card: resolve_entity {type:event} → event_card {includeBouts:true} → match_preview for a featured bout.
|
|
63
|
+
Match preview (named sides): resolve_entity each side (optional) → match_preview {teamA, teamB}.
|
|
64
|
+
App scaffold: list_capabilities ∥ api_health ∥ live_matches, then one composite per screen.
|
|
65
|
+
|
|
66
|
+
## Resources
|
|
67
|
+
|
|
68
|
+
- cito://llms.txt — product context when available
|
|
69
|
+
- cito://capabilities — catalog summary
|
|
70
|
+
- cito://openapi.json — optional public OpenAPI fetch for typed clients
|
|
71
|
+
|
|
72
|
+
## What not to do
|
|
73
|
+
|
|
74
|
+
- Invent match/player/team IDs from memory.
|
|
75
|
+
- Parallel-paginate the same list with different cursors.
|
|
76
|
+
- N+1 match_details for every live row.
|
|
77
|
+
- Retry UNSUPPORTED_GAME or VALIDATION unchanged.
|
|
78
|
+
- Ship production traffic through MCP.
|
|
79
|
+
- Flood odds/timelines when a summary answers the question.
|
|
80
|
+
`;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { metaTools } from './meta.js';
|
|
2
|
+
import { resolveTools } from './resolve.js';
|
|
3
|
+
import { liveTools } from './live.js';
|
|
4
|
+
import { matchTools } from './match.js';
|
|
5
|
+
import { playerTools } from './player.js';
|
|
6
|
+
import { teamTools } from './team.js';
|
|
7
|
+
import { standingsTools } from './standings.js';
|
|
8
|
+
import { insightTools } from './insight.js';
|
|
9
|
+
/** Curated outcome-tool catalog (15 tools). Order matches preferred cold-start ladder. */
|
|
10
|
+
export const allTools = [
|
|
11
|
+
...metaTools.filter((t) => t.name === 'list_capabilities' || t.name === 'api_health'),
|
|
12
|
+
...resolveTools,
|
|
13
|
+
...liveTools,
|
|
14
|
+
...matchTools,
|
|
15
|
+
...playerTools,
|
|
16
|
+
...teamTools,
|
|
17
|
+
...standingsTools,
|
|
18
|
+
...insightTools,
|
|
19
|
+
...metaTools.filter((t) => t.name === 'call_api'),
|
|
20
|
+
];
|
|
21
|
+
export function getTool(name) {
|
|
22
|
+
return allTools.find((t) => t.name === name);
|
|
23
|
+
}
|