cito-mcp 0.2.6 → 0.3.0
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 +26 -10
- package/dist/envelope.js +23 -0
- package/dist/http.js +150 -0
- package/dist/index.js +113 -33
- package/dist/install.js +348 -0
- package/dist/instructions.js +1 -1
- package/dist/tools/live.js +17 -0
- package/dist/tools/match.js +32 -28
- package/dist/tools/meta.js +4 -2
- package/dist/tools/normalize.js +21 -3
- package/dist/tools/player.js +40 -1
- package/dist/tools/resolve.js +47 -0
- package/dist/tools/standings.js +9 -1
- package/dist/tools/team.js +109 -27
- package/dist/tools/types.js +21 -6
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 --
|
|
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
|
|
@@ -376,6 +382,9 @@ CITO_API_KEY=cito_your_key_here node dist/index.js --http 8787
|
|
|
376
382
|
# → http://127.0.0.1:8787/mcp
|
|
377
383
|
```
|
|
378
384
|
|
|
385
|
+
Hosted (production): `https://api.citoapi.com/mcp`
|
|
386
|
+
Send the user's Cito key as `x-api-key`. Initialize and `list_capabilities` work without a key so Smithery can scan.
|
|
387
|
+
|
|
379
388
|
---
|
|
380
389
|
|
|
381
390
|
## Migration from 0.1 (auto-generated tools)
|
|
@@ -460,13 +469,20 @@ Package: **`cito-mcp@0.2.4`**
|
|
|
460
469
|
- `live_matches`
|
|
461
470
|
- `resolve_entity` (e.g. T1 / s1mple)
|
|
462
471
|
- `match_summary` with a real `matchId` from live/schedule
|
|
463
|
-
4. Confirm `package.json` version and README match the shipped tool list
|
|
472
|
+
4. Confirm `package.json` version and README match the shipped tool list.
|
|
464
473
|
5. `npm publish` from `mcp/` (or your release pipeline) with appropriate npm auth / access.
|
|
474
|
+
`prepublishOnly` runs build + tests + smoke, so a broken build cannot ship.
|
|
465
475
|
|
|
466
476
|
### Install line for docs & marketing
|
|
467
477
|
|
|
468
478
|
```bash
|
|
469
|
-
|
|
479
|
+
npx cito-mcp install --key cito_…
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
Manual fallback (note the quoted `--`, required for PowerShell):
|
|
483
|
+
|
|
484
|
+
```bash
|
|
485
|
+
claude mcp add cito -e CITO_API_KEY=cito_… "--" npx -y cito-mcp
|
|
470
486
|
```
|
|
471
487
|
|
|
472
488
|
```json
|
package/dist/envelope.js
CHANGED
|
@@ -201,10 +201,33 @@ export function sealEnvelope(envelope) {
|
|
|
201
201
|
},
|
|
202
202
|
};
|
|
203
203
|
}
|
|
204
|
+
/** Shared tool output for OpenAI plugin scan. Matches the JSON envelope. */
|
|
205
|
+
export const ENVELOPE_OUTPUT_SCHEMA = {
|
|
206
|
+
type: 'object',
|
|
207
|
+
additionalProperties: true,
|
|
208
|
+
required: ['ok', 'meta'],
|
|
209
|
+
properties: {
|
|
210
|
+
ok: { type: 'boolean', description: 'true if the tool succeeded' },
|
|
211
|
+
data: { description: 'Result payload when ok is true; null on error' },
|
|
212
|
+
error: {
|
|
213
|
+
type: 'object',
|
|
214
|
+
additionalProperties: true,
|
|
215
|
+
properties: {
|
|
216
|
+
code: { type: 'string' },
|
|
217
|
+
message: { type: 'string' },
|
|
218
|
+
recover: { type: 'array', items: { type: 'string' } },
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
meta: { type: 'object', additionalProperties: true },
|
|
222
|
+
pagination: { type: 'object', additionalProperties: true },
|
|
223
|
+
partial: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
224
|
+
},
|
|
225
|
+
};
|
|
204
226
|
export function toMcpResult(envelope) {
|
|
205
227
|
const sealed = sealEnvelope(envelope);
|
|
206
228
|
return {
|
|
207
229
|
content: [{ type: 'text', text: JSON.stringify(sealed, null, 2) }],
|
|
230
|
+
structuredContent: sealed,
|
|
208
231
|
...(sealed.ok ? {} : { isError: true }),
|
|
209
232
|
};
|
|
210
233
|
}
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
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
|
+
import { allTools } from './tools/index.js';
|
|
9
|
+
import { toolListing } from './tools/types.js';
|
|
10
|
+
import { PACKAGE_VERSION } from './version.js';
|
|
11
|
+
export const MCP_PATH = '/mcp';
|
|
12
|
+
export const MCP_CONFIG_PATH = '/.well-known/mcp-config';
|
|
13
|
+
export const MCP_SERVER_CARD_PATH = '/.well-known/mcp/server-card.json';
|
|
14
|
+
const CORS_HEADERS = {
|
|
15
|
+
'access-control-allow-origin': '*',
|
|
16
|
+
'access-control-allow-methods': 'GET, POST, DELETE, OPTIONS, HEAD',
|
|
17
|
+
'access-control-allow-headers': 'Content-Type, Accept, Authorization, x-api-key, x-cito-api-key, mcp-session-id, mcp-protocol-version, last-event-id',
|
|
18
|
+
'access-control-expose-headers': 'mcp-session-id, mcp-protocol-version',
|
|
19
|
+
};
|
|
20
|
+
export function requestPathname(url) {
|
|
21
|
+
if (!url)
|
|
22
|
+
return '';
|
|
23
|
+
try {
|
|
24
|
+
return new URL(url, 'http://127.0.0.1').pathname;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
const q = url.indexOf('?');
|
|
28
|
+
return q === -1 ? url : url.slice(0, q);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function headerValue(headers, name) {
|
|
32
|
+
const raw = headers[name] ?? headers[name.toLowerCase()];
|
|
33
|
+
if (Array.isArray(raw))
|
|
34
|
+
return (raw[0] ?? '').trim();
|
|
35
|
+
return typeof raw === 'string' ? raw.trim() : '';
|
|
36
|
+
}
|
|
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
|
+
export function extractApiKey(req) {
|
|
42
|
+
const fromHeader = headerValue(req.headers, 'x-api-key') || headerValue(req.headers, 'x-cito-api-key');
|
|
43
|
+
if (fromHeader)
|
|
44
|
+
return fromHeader;
|
|
45
|
+
const auth = headerValue(req.headers, 'authorization');
|
|
46
|
+
const bearer = /^bearer\s+/i.exec(auth);
|
|
47
|
+
if (bearer) {
|
|
48
|
+
const token = auth.slice(bearer[0].length).trim();
|
|
49
|
+
if (token)
|
|
50
|
+
return token;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const u = new URL(req.url ?? '', 'http://127.0.0.1');
|
|
54
|
+
return (u.searchParams.get('apiKey') ?? u.searchParams.get('CITO_API_KEY') ?? '').trim();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function corsHeaders() {
|
|
61
|
+
return { ...CORS_HEADERS };
|
|
62
|
+
}
|
|
63
|
+
const MCP_ACCEPT = 'application/json, text/event-stream';
|
|
64
|
+
const LEGACY_PROTOCOL = '2025-11-25';
|
|
65
|
+
/** Node reads Accept from rawHeaders; mutating headers.accept alone is not enough. */
|
|
66
|
+
export function setIncomingHeader(req, name, value) {
|
|
67
|
+
req.headers[name.toLowerCase()] = value;
|
|
68
|
+
const raw = req.rawHeaders;
|
|
69
|
+
if (!Array.isArray(raw))
|
|
70
|
+
return;
|
|
71
|
+
const lower = name.toLowerCase();
|
|
72
|
+
for (let i = 0; i < raw.length; i += 2) {
|
|
73
|
+
if (String(raw[i]).toLowerCase() === lower) {
|
|
74
|
+
raw[i + 1] = value;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
raw.push(name, value);
|
|
79
|
+
}
|
|
80
|
+
/** SDK 406s POSTs that omit MCP Accept. OpenAI's scanner often omits it. */
|
|
81
|
+
export function ensureMcpAccept(headers, req) {
|
|
82
|
+
const raw = headers.accept ?? headers.Accept;
|
|
83
|
+
const value = Array.isArray(raw) ? raw.join(',') : typeof raw === 'string' ? raw : '';
|
|
84
|
+
const lower = value.toLowerCase();
|
|
85
|
+
if (lower.includes('application/json') && lower.includes('text/event-stream'))
|
|
86
|
+
return;
|
|
87
|
+
if (req)
|
|
88
|
+
setIncomingHeader(req, 'accept', MCP_ACCEPT);
|
|
89
|
+
else
|
|
90
|
+
headers.accept = MCP_ACCEPT;
|
|
91
|
+
}
|
|
92
|
+
/** OpenAI Scan Tools sends 2026-07-28; this SDK only speaks 2025-era. */
|
|
93
|
+
export function rewriteMcpProtocolMessage(body) {
|
|
94
|
+
if (!body || typeof body !== 'object')
|
|
95
|
+
return body;
|
|
96
|
+
const msg = body;
|
|
97
|
+
const params = msg.params;
|
|
98
|
+
if (params && typeof params === 'object') {
|
|
99
|
+
const p = params;
|
|
100
|
+
if (p.protocolVersion === '2026-07-28')
|
|
101
|
+
p.protocolVersion = LEGACY_PROTOCOL;
|
|
102
|
+
const meta = p._meta;
|
|
103
|
+
if (meta && typeof meta === 'object') {
|
|
104
|
+
const m = meta;
|
|
105
|
+
const key = 'io.modelcontextprotocol/protocolVersion';
|
|
106
|
+
if (m[key] === '2026-07-28')
|
|
107
|
+
m[key] = LEGACY_PROTOCOL;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return body;
|
|
111
|
+
}
|
|
112
|
+
/** Smithery session config: API key as x-api-key, not OAuth. */
|
|
113
|
+
export function mcpConfigSchema() {
|
|
114
|
+
return {
|
|
115
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
116
|
+
type: 'object',
|
|
117
|
+
title: 'Cito API',
|
|
118
|
+
description: 'Cito esports data for agents. Get a key at https://citoapi.com/dashboard',
|
|
119
|
+
required: ['apiKey'],
|
|
120
|
+
properties: {
|
|
121
|
+
apiKey: {
|
|
122
|
+
type: 'string',
|
|
123
|
+
title: 'Cito API Key',
|
|
124
|
+
description: 'Your Cito API key from https://citoapi.com/dashboard',
|
|
125
|
+
'x-from': { header: 'x-api-key' },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Scan fallback if Smithery cannot complete initialize. No OAuth. */
|
|
131
|
+
export function mcpServerCard() {
|
|
132
|
+
return {
|
|
133
|
+
serverInfo: {
|
|
134
|
+
name: 'cito-mcp',
|
|
135
|
+
version: PACKAGE_VERSION,
|
|
136
|
+
title: 'Cito API',
|
|
137
|
+
websiteUrl: 'https://citoapi.com',
|
|
138
|
+
},
|
|
139
|
+
authentication: {
|
|
140
|
+
required: false,
|
|
141
|
+
},
|
|
142
|
+
tools: allTools.map(toolListing),
|
|
143
|
+
resources: [
|
|
144
|
+
{ uri: 'cito://capabilities', name: 'cito-mcp capabilities summary' },
|
|
145
|
+
{ uri: 'cito://llms.txt', name: 'Cito API agent context (llms.txt)' },
|
|
146
|
+
{ uri: 'cito://openapi.json', name: 'Cito OpenAPI (public fetch)' },
|
|
147
|
+
],
|
|
148
|
+
prompts: [],
|
|
149
|
+
};
|
|
150
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* 14 outcome tools (not OpenAPI mass-generation). stdio by default;
|
|
6
6
|
* `--http <port>` serves a stateless Streamable HTTP endpoint.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* Optional
|
|
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
10
|
*/
|
|
11
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
11
12
|
import { createServer as createHttpServer } from 'node:http';
|
|
12
13
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
13
14
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
@@ -17,9 +18,26 @@ import { DEFAULT_API_BASE, log } from './client.js';
|
|
|
17
18
|
import { errorEnvelope, toMcpResult } from './envelope.js';
|
|
18
19
|
import { SERVER_INSTRUCTIONS } from './instructions.js';
|
|
19
20
|
import { allTools, getTool } from './tools/index.js';
|
|
20
|
-
import { runTool } from './tools/types.js';
|
|
21
|
+
import { runTool, toolListing } from './tools/types.js';
|
|
21
22
|
import { PACKAGE_VERSION } from './version.js';
|
|
22
|
-
|
|
23
|
+
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
|
+
if (process.argv[2] === 'install') {
|
|
33
|
+
const { runInstall } = await import('./install.js');
|
|
34
|
+
process.exit(await runInstall(process.argv.slice(3)));
|
|
35
|
+
}
|
|
36
|
+
const ENV_API_KEY = process.env.CITO_API_KEY ?? '';
|
|
37
|
+
const requestAuth = new AsyncLocalStorage();
|
|
38
|
+
function currentApiKey() {
|
|
39
|
+
return requestAuth.getStore()?.apiKey || ENV_API_KEY;
|
|
40
|
+
}
|
|
23
41
|
/**
|
|
24
42
|
* Tools that need no API key. These must keep working with the server
|
|
25
43
|
* unconfigured, so a user can add it and immediately see what it does.
|
|
@@ -38,20 +56,21 @@ const OFFLINE_TOOLS = new Set(['list_capabilities']);
|
|
|
38
56
|
* recovery steps, which the model can read out; list_capabilities keeps working
|
|
39
57
|
* offline so the server is browsable before it is configured.
|
|
40
58
|
*/
|
|
41
|
-
if (!
|
|
59
|
+
if (!ENV_API_KEY) {
|
|
42
60
|
// stderr only — stdout is the JSON-RPC channel and must stay clean.
|
|
43
61
|
console.error('[cito-mcp] CITO_API_KEY is not set. Serving tool catalog only; ' +
|
|
44
|
-
'API-backed tools will return MISSING_API_KEY until a key is provided
|
|
45
|
-
'
|
|
62
|
+
'API-backed tools will return MISSING_API_KEY until a key is provided ' +
|
|
63
|
+
'(env CITO_API_KEY, or x-api-key on hosted HTTP). ' +
|
|
64
|
+
'Get one at https://citoapi.com/dashboard.');
|
|
46
65
|
}
|
|
47
66
|
const API_BASE = (process.env.CITO_API_BASE || DEFAULT_API_BASE).replace(/\/+$/, '');
|
|
48
67
|
const ctx = {
|
|
49
68
|
// Empty string when unconfigured; the dispatcher blocks API-backed tools
|
|
50
69
|
// before any request is attempted, so this is never sent as a credential.
|
|
51
|
-
apiKey:
|
|
70
|
+
apiKey: ENV_API_KEY,
|
|
52
71
|
baseUrl: API_BASE,
|
|
53
72
|
};
|
|
54
|
-
|
|
73
|
+
function createCitoServer() {
|
|
55
74
|
const server = new Server({
|
|
56
75
|
name: 'cito-mcp',
|
|
57
76
|
version: PACKAGE_VERSION,
|
|
@@ -60,11 +79,7 @@ async function main() {
|
|
|
60
79
|
instructions: SERVER_INSTRUCTIONS,
|
|
61
80
|
});
|
|
62
81
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
63
|
-
tools: allTools.map(
|
|
64
|
-
name,
|
|
65
|
-
description,
|
|
66
|
-
inputSchema,
|
|
67
|
-
})),
|
|
82
|
+
tools: allTools.map(toolListing),
|
|
68
83
|
}));
|
|
69
84
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
70
85
|
const { name } = request.params;
|
|
@@ -81,7 +96,8 @@ async function main() {
|
|
|
81
96
|
}
|
|
82
97
|
// Fail the CALL, not the process. The model gets an actionable envelope it
|
|
83
98
|
// can relay verbatim instead of the client reporting a dead server.
|
|
84
|
-
|
|
99
|
+
const apiKey = currentApiKey();
|
|
100
|
+
if (!apiKey && !OFFLINE_TOOLS.has(name)) {
|
|
85
101
|
return toMcpResult(errorEnvelope({
|
|
86
102
|
code: 'UNAUTHORIZED',
|
|
87
103
|
message: 'CITO_API_KEY is not set, so this tool cannot reach the Cito API. ' +
|
|
@@ -92,13 +108,14 @@ async function main() {
|
|
|
92
108
|
retryable: false,
|
|
93
109
|
recover: [
|
|
94
110
|
'Create a key at https://citoapi.com/dashboard',
|
|
95
|
-
'
|
|
111
|
+
'Hosted: send it as the x-api-key header to https://api.citoapi.com/mcp',
|
|
112
|
+
'Local: claude mcp add cito -e CITO_API_KEY=cito_... -- npx cito-mcp',
|
|
96
113
|
'Or set CITO_API_KEY in the mcpServers env block of your client config',
|
|
97
114
|
'Call list_capabilities to browse available tools without a key',
|
|
98
115
|
],
|
|
99
116
|
}));
|
|
100
117
|
}
|
|
101
|
-
return runTool(tool, args, ctx);
|
|
118
|
+
return runTool(tool, args, { ...ctx, apiKey });
|
|
102
119
|
});
|
|
103
120
|
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
104
121
|
resources: [
|
|
@@ -169,7 +186,7 @@ async function main() {
|
|
|
169
186
|
if (uri === 'cito://openapi.json') {
|
|
170
187
|
try {
|
|
171
188
|
const response = await fetch(`${API_BASE}/openapi.json`, {
|
|
172
|
-
headers: { 'x-api-key':
|
|
189
|
+
headers: { 'x-api-key': currentApiKey(), accept: 'application/json' },
|
|
173
190
|
});
|
|
174
191
|
const text = await response.text();
|
|
175
192
|
return {
|
|
@@ -190,6 +207,9 @@ async function main() {
|
|
|
190
207
|
}
|
|
191
208
|
throw new Error(`unknown resource: ${uri}`);
|
|
192
209
|
});
|
|
210
|
+
return server;
|
|
211
|
+
}
|
|
212
|
+
async function main() {
|
|
193
213
|
const httpPort = (() => {
|
|
194
214
|
const index = process.argv.indexOf('--http');
|
|
195
215
|
if (index === -1)
|
|
@@ -198,33 +218,93 @@ async function main() {
|
|
|
198
218
|
return Number.isInteger(port) && port > 0 ? port : null;
|
|
199
219
|
})();
|
|
200
220
|
if (httpPort) {
|
|
201
|
-
const
|
|
202
|
-
await server.connect(transport);
|
|
221
|
+
const listenHost = process.env.CITO_MCP_LISTEN || '127.0.0.1';
|
|
203
222
|
const httpServer = createHttpServer(async (req, res) => {
|
|
204
|
-
|
|
223
|
+
const path = requestPathname(req.url);
|
|
224
|
+
const cors = corsHeaders();
|
|
225
|
+
for (const [k, v] of Object.entries(cors))
|
|
226
|
+
res.setHeader(k, v);
|
|
227
|
+
if (req.method === 'OPTIONS') {
|
|
228
|
+
res.writeHead(204);
|
|
229
|
+
res.end();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if ((path === MCP_CONFIG_PATH || path === MCP_SERVER_CARD_PATH) &&
|
|
233
|
+
(req.method === 'GET' || req.method === 'HEAD')) {
|
|
234
|
+
const payload = path === MCP_CONFIG_PATH ? mcpConfigSchema() : mcpServerCard();
|
|
235
|
+
const body = JSON.stringify(payload);
|
|
236
|
+
res.writeHead(200, {
|
|
237
|
+
'content-type': 'application/json; charset=utf-8',
|
|
238
|
+
'cache-control': 'public, max-age=60',
|
|
239
|
+
});
|
|
240
|
+
if (req.method === 'HEAD') {
|
|
241
|
+
res.end();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
res.end(body);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (path !== MCP_PATH) {
|
|
205
248
|
res.writeHead(404, { 'content-type': 'application/json' });
|
|
206
249
|
res.end(JSON.stringify({ error: 'use POST /mcp' }));
|
|
207
250
|
return;
|
|
208
251
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
252
|
+
const apiKey = extractApiKey(req);
|
|
253
|
+
ensureMcpAccept(req.headers, req);
|
|
254
|
+
const proto = req.headers['mcp-protocol-version'];
|
|
255
|
+
const protoVal = Array.isArray(proto) ? proto[0] : proto;
|
|
256
|
+
if (protoVal === '2026-07-28') {
|
|
257
|
+
setIncomingHeader(req, 'mcp-protocol-version', '2025-11-25');
|
|
258
|
+
}
|
|
259
|
+
const server = createCitoServer();
|
|
260
|
+
const transport = new StreamableHTTPServerTransport({
|
|
261
|
+
sessionIdGenerator: undefined,
|
|
262
|
+
enableJsonResponse: true,
|
|
263
|
+
});
|
|
264
|
+
transport.onerror = (error) => {
|
|
265
|
+
log(`http transport: ${error instanceof Error ? error.message : String(error)}`);
|
|
266
|
+
};
|
|
267
|
+
res.on('close', () => {
|
|
268
|
+
void transport.close();
|
|
269
|
+
});
|
|
213
270
|
try {
|
|
214
|
-
|
|
271
|
+
await server.connect(transport);
|
|
272
|
+
await requestAuth.run({ apiKey }, async () => {
|
|
273
|
+
if (req.method === 'POST') {
|
|
274
|
+
let raw = '';
|
|
275
|
+
for await (const chunk of req)
|
|
276
|
+
raw += chunk;
|
|
277
|
+
let parsed = null;
|
|
278
|
+
if (raw.trim()) {
|
|
279
|
+
try {
|
|
280
|
+
parsed = rewriteMcpProtocolMessage(JSON.parse(raw));
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
res.writeHead(400, { 'content-type': 'application/json' });
|
|
284
|
+
res.end(JSON.stringify({ error: 'invalid JSON body' }));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
await transport.handleRequest(req, res, parsed);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
await transport.handleRequest(req, res);
|
|
292
|
+
});
|
|
215
293
|
}
|
|
216
|
-
catch {
|
|
217
|
-
|
|
218
|
-
res.
|
|
219
|
-
|
|
294
|
+
catch (err) {
|
|
295
|
+
log(`http handler error: ${err.message}`);
|
|
296
|
+
if (!res.headersSent) {
|
|
297
|
+
res.writeHead(500, { 'content-type': 'application/json' });
|
|
298
|
+
res.end(JSON.stringify({ error: 'mcp handler failed' }));
|
|
299
|
+
}
|
|
220
300
|
}
|
|
221
|
-
await transport.handleRequest(req, res, body);
|
|
222
301
|
});
|
|
223
|
-
httpServer.listen(httpPort, () => {
|
|
224
|
-
log(`cito-mcp ${PACKAGE_VERSION} listening on http
|
|
302
|
+
httpServer.listen(httpPort, listenHost, () => {
|
|
303
|
+
log(`cito-mcp ${PACKAGE_VERSION} listening on http://${listenHost}:${httpPort}/mcp (${allTools.length} curated tools)`);
|
|
225
304
|
});
|
|
226
305
|
}
|
|
227
306
|
else {
|
|
307
|
+
const server = createCitoServer();
|
|
228
308
|
await server.connect(new StdioServerTransport());
|
|
229
309
|
log(`cito-mcp ${PACKAGE_VERSION} on stdio (${allTools.length} curated tools)`);
|
|
230
310
|
}
|