@onvcodes/mcp-server 0.0.0-stage → 1.0.1
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 +181 -2
- package/dist/server.cjs +514 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +33 -0
- package/dist/server.d.ts +33 -0
- package/dist/server.js +507 -0
- package/dist/server.js.map +1 -0
- package/package.json +48 -4
package/dist/server.js
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
5
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
6
|
+
import http from 'http';
|
|
7
|
+
import { randomUUID } from 'crypto';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
|
|
10
|
+
// ../packages/sdk-ts/dist/index.js
|
|
11
|
+
var STATUS_CODE = {
|
|
12
|
+
400: "INVALID_REQUEST",
|
|
13
|
+
401: "UNAUTHORIZED",
|
|
14
|
+
403: "FORBIDDEN",
|
|
15
|
+
404: "NOT_FOUND",
|
|
16
|
+
405: "METHOD_NOT_ALLOWED",
|
|
17
|
+
409: "CONFLICT",
|
|
18
|
+
422: "UNPROCESSABLE_ENTITY",
|
|
19
|
+
429: "RATE_LIMITED",
|
|
20
|
+
500: "INTERNAL_ERROR",
|
|
21
|
+
502: "BAD_GATEWAY",
|
|
22
|
+
503: "SERVICE_UNAVAILABLE",
|
|
23
|
+
504: "GATEWAY_TIMEOUT"
|
|
24
|
+
};
|
|
25
|
+
var STATUS_MESSAGE = {
|
|
26
|
+
400: "Bad Request",
|
|
27
|
+
401: "Unauthorized",
|
|
28
|
+
403: "Forbidden",
|
|
29
|
+
404: "Not Found",
|
|
30
|
+
405: "Method Not Allowed",
|
|
31
|
+
409: "Conflict",
|
|
32
|
+
422: "Unprocessable Entity",
|
|
33
|
+
429: "Too Many Requests",
|
|
34
|
+
500: "Internal Server Error",
|
|
35
|
+
502: "Bad Gateway",
|
|
36
|
+
503: "Service Unavailable",
|
|
37
|
+
504: "Gateway Timeout"
|
|
38
|
+
};
|
|
39
|
+
function stringifyDetail(value) {
|
|
40
|
+
if (typeof value === "string") return value;
|
|
41
|
+
if (value === void 0 || value === null) return "";
|
|
42
|
+
try {
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
} catch {
|
|
45
|
+
return String(value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
var ONVError = class _ONVError extends Error {
|
|
49
|
+
/** 错误码(string)。 */
|
|
50
|
+
code;
|
|
51
|
+
/** HTTP 状态码;网络错误为 0。 */
|
|
52
|
+
statusCode;
|
|
53
|
+
/** 详细说明。 */
|
|
54
|
+
detail;
|
|
55
|
+
/** 可选的处理建议。 */
|
|
56
|
+
action;
|
|
57
|
+
constructor(message, code, statusCode, detail = "", action) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = "ONVError";
|
|
60
|
+
this.code = code;
|
|
61
|
+
this.statusCode = statusCode;
|
|
62
|
+
this.detail = detail;
|
|
63
|
+
this.action = action;
|
|
64
|
+
}
|
|
65
|
+
/** 是否为网络层错误(fetch 失败 / 超时)。 */
|
|
66
|
+
get isNetworkError() {
|
|
67
|
+
return this.code === "NETWORK_ERROR";
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 从 HTTP 响应构造错误。
|
|
71
|
+
* 响应体按 `{ code, detail, action }` 解析(同时兼容 `{ message }`);
|
|
72
|
+
* 缺少 code 时按 statusCode 映射(如 404 -> NOT_FOUND)。
|
|
73
|
+
*/
|
|
74
|
+
static fromHttpResponse(status, body) {
|
|
75
|
+
const parsed = body && typeof body === "object" ? body : {};
|
|
76
|
+
const rawCode = parsed.code;
|
|
77
|
+
const code = typeof rawCode === "string" || typeof rawCode === "number" ? String(rawCode) : STATUS_CODE[status] ?? `HTTP_${status}`;
|
|
78
|
+
const detail = stringifyDetail(parsed.detail);
|
|
79
|
+
const action = typeof parsed.action === "string" ? parsed.action : void 0;
|
|
80
|
+
const message = typeof parsed.message === "string" && parsed.message ? parsed.message : detail ? `HTTP ${status}: ${detail}` : `HTTP ${status} ${STATUS_MESSAGE[status] ?? ""}`.trim();
|
|
81
|
+
return new _ONVError(message, code, status, detail, action);
|
|
82
|
+
}
|
|
83
|
+
/** 网络失败(fetch 抛错 / 超时 / DNS 等)→ code = NETWORK_ERROR, statusCode = 0。 */
|
|
84
|
+
static networkError(original) {
|
|
85
|
+
const cause = original instanceof Error ? original : new Error(String(original));
|
|
86
|
+
return new _ONVError(
|
|
87
|
+
`Network error: ${cause.message}`,
|
|
88
|
+
"NETWORK_ERROR",
|
|
89
|
+
0,
|
|
90
|
+
cause.message
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// src/auth.ts
|
|
96
|
+
function headerValue(value) {
|
|
97
|
+
if (Array.isArray(value)) return value[0]?.trim() ?? "";
|
|
98
|
+
return (value ?? "").trim();
|
|
99
|
+
}
|
|
100
|
+
function resolveCredential(headers) {
|
|
101
|
+
const authorization = headerValue(headers.authorization);
|
|
102
|
+
if (authorization) {
|
|
103
|
+
const matched = /^Bearer\s+(.+)$/i.exec(authorization);
|
|
104
|
+
if (matched?.[1]?.trim()) {
|
|
105
|
+
return { type: "bearer", accessToken: matched[1].trim() };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const apiKey = headerValue(headers["x-api-key"]);
|
|
109
|
+
if (apiKey) return { type: "apikey", apiKey };
|
|
110
|
+
return { type: "anonymous" };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/oauth.ts
|
|
114
|
+
function oauthIssuer() {
|
|
115
|
+
return String(process.env.MCP_OAUTH_ISSUER || "https://onv.codes").replace(/\/+$/, "");
|
|
116
|
+
}
|
|
117
|
+
function resourceIdentifier() {
|
|
118
|
+
return String(process.env.MCP_RESOURCE_URL || `${oauthIssuer()}/mcp`).replace(/\/+$/, "");
|
|
119
|
+
}
|
|
120
|
+
function protectedResourceMetadataUrl() {
|
|
121
|
+
return `${oauthIssuer()}/.well-known/oauth-protected-resource`;
|
|
122
|
+
}
|
|
123
|
+
function protectedResourceMetadata() {
|
|
124
|
+
return {
|
|
125
|
+
resource: resourceIdentifier(),
|
|
126
|
+
authorization_servers: [oauthIssuer()],
|
|
127
|
+
bearer_methods_supported: ["header"],
|
|
128
|
+
scopes_supported: ["mcp:read", "mcp:write", "mcp:trade", "mcp:backtest"],
|
|
129
|
+
resource_name: "ONV Platform MCP",
|
|
130
|
+
resource_documentation: `${oauthIssuer()}/mcp-setup`
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function oauthChallengeEnabled() {
|
|
134
|
+
return String(process.env.MCP_OAUTH_CHALLENGE || "").toLowerCase() === "true";
|
|
135
|
+
}
|
|
136
|
+
function hasCredential(headers) {
|
|
137
|
+
const auth = headers.authorization;
|
|
138
|
+
const apiKey = headers["x-api-key"];
|
|
139
|
+
return Boolean((Array.isArray(auth) ? auth[0] : auth) || (Array.isArray(apiKey) ? apiKey[0] : apiKey));
|
|
140
|
+
}
|
|
141
|
+
function sendOAuthChallenge(res) {
|
|
142
|
+
res.writeHead(401, {
|
|
143
|
+
"Content-Type": "application/json",
|
|
144
|
+
"WWW-Authenticate": `Bearer resource_metadata="${protectedResourceMetadataUrl()}"`
|
|
145
|
+
});
|
|
146
|
+
res.end(JSON.stringify({
|
|
147
|
+
jsonrpc: "2.0",
|
|
148
|
+
error: { code: -32001, message: "Unauthorized: OAuth bearer token required" },
|
|
149
|
+
id: null
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/config.ts
|
|
154
|
+
function loadConfig(env = process.env) {
|
|
155
|
+
return {
|
|
156
|
+
apiKey: env.ONV_API_KEY || void 0,
|
|
157
|
+
baseUrl: env.ONV_BASE_URL || "https://onv.codes"
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function jsonSchemaToZod(schema) {
|
|
161
|
+
const shape = {};
|
|
162
|
+
if (!schema || typeof schema !== "object") return shape;
|
|
163
|
+
const props = schema.properties ?? {};
|
|
164
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
165
|
+
for (const [key, p] of Object.entries(props)) {
|
|
166
|
+
const desc = p?.description ?? "";
|
|
167
|
+
let zt;
|
|
168
|
+
switch (p?.type) {
|
|
169
|
+
case "number":
|
|
170
|
+
zt = z.number();
|
|
171
|
+
break;
|
|
172
|
+
case "boolean":
|
|
173
|
+
zt = z.boolean();
|
|
174
|
+
break;
|
|
175
|
+
case "array":
|
|
176
|
+
zt = z.array(z.any());
|
|
177
|
+
break;
|
|
178
|
+
case "object":
|
|
179
|
+
zt = z.record(z.string(), z.any());
|
|
180
|
+
break;
|
|
181
|
+
default:
|
|
182
|
+
zt = z.string();
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
let field = required.includes(key) ? zt : zt.optional();
|
|
186
|
+
if (desc) field = field.describe(desc);
|
|
187
|
+
shape[key] = field;
|
|
188
|
+
}
|
|
189
|
+
return shape;
|
|
190
|
+
}
|
|
191
|
+
async function safeRun(fn) {
|
|
192
|
+
try {
|
|
193
|
+
const result = await fn();
|
|
194
|
+
const text = result === void 0 ? "OK" : JSON.stringify(result, null, 2);
|
|
195
|
+
return { content: [{ type: "text", text }] };
|
|
196
|
+
} catch (err) {
|
|
197
|
+
return {
|
|
198
|
+
content: [
|
|
199
|
+
{
|
|
200
|
+
type: "text",
|
|
201
|
+
text: JSON.stringify(
|
|
202
|
+
{ error: { code: "PROXY_ERROR", message: err?.message || String(err) } },
|
|
203
|
+
null,
|
|
204
|
+
2
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
],
|
|
208
|
+
isError: true
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function discoverTools(config) {
|
|
213
|
+
try {
|
|
214
|
+
const res = await fetch(`${config.baseUrl.replace(/\/+$/, "")}/api/mcp/platform/tools`, {
|
|
215
|
+
signal: AbortSignal.timeout(15e3)
|
|
216
|
+
});
|
|
217
|
+
if (!res.ok) return null;
|
|
218
|
+
const json = await res.json().catch(() => null);
|
|
219
|
+
return json?.data?.tools ?? null;
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
var GROUP_LABELS = {
|
|
225
|
+
market: "\u884C\u60C5",
|
|
226
|
+
strategy: "\u7B56\u7565",
|
|
227
|
+
portfolio: "\u6301\u4ED3\u4E0E\u6267\u884C",
|
|
228
|
+
account: "\u8D26\u6237\u4E0E\u989D\u5EA6"
|
|
229
|
+
};
|
|
230
|
+
function humanizeToolName(name) {
|
|
231
|
+
return name.replace(/^platform_/, "").replace(/_/g, " ");
|
|
232
|
+
}
|
|
233
|
+
async function registerTools(server, config) {
|
|
234
|
+
const base = config.baseUrl.replace(/\/+$/, "");
|
|
235
|
+
const headers = { "content-type": "application/json" };
|
|
236
|
+
if (config.accessToken) headers["authorization"] = `Bearer ${config.accessToken}`;
|
|
237
|
+
else if (config.apiKey) headers["x-api-key"] = config.apiKey;
|
|
238
|
+
const tools = await discoverTools(config);
|
|
239
|
+
if (!tools || tools.length === 0) {
|
|
240
|
+
server.registerTool(
|
|
241
|
+
"__platform_health",
|
|
242
|
+
{
|
|
243
|
+
title: "Platform Health",
|
|
244
|
+
description: "\u5E73\u53F0 MCP \u7AEF\u70B9\u72B6\u6001\u3002\u5DE5\u5177\u672A\u52A0\u8F7D\u65F6\u7528\u4E8E\u6392\u67E5\uFF1A\u68C0\u67E5 ONV_BASE_URL \u4E0E ONV_API_KEY \u914D\u7F6E\u3002",
|
|
245
|
+
inputSchema: {}
|
|
246
|
+
},
|
|
247
|
+
async () => safeRun(() => ({
|
|
248
|
+
ok: false,
|
|
249
|
+
error: "platform tools unavailable",
|
|
250
|
+
detail: tools === null ? "\u65E0\u6CD5\u4ECE\u5E73\u53F0\u83B7\u53D6\u5DE5\u5177\u6E05\u5355\uFF08\u68C0\u67E5 ONV_BASE_URL \u4E0E\u7F51\u7EDC\uFF09" : "\u5E73\u53F0\u5DE5\u5177\u5217\u8868\u4E3A\u7A7A\uFF08\u68C0\u67E5 ENABLE_PLATFORM_TOOLS / PLATFORM_TOOL_PUBLIC_URL\uFF09",
|
|
251
|
+
baseUrl: base,
|
|
252
|
+
hasApiKey: !!config.apiKey
|
|
253
|
+
}))
|
|
254
|
+
);
|
|
255
|
+
return 0;
|
|
256
|
+
}
|
|
257
|
+
for (const tool of tools) {
|
|
258
|
+
const title = tool.title || humanizeToolName(tool.name);
|
|
259
|
+
const isWrite = tool.mode === "write";
|
|
260
|
+
const groupLabel = tool.group ? GROUP_LABELS[tool.group] : "";
|
|
261
|
+
const prefix = groupLabel ? `\u3010${groupLabel}\u3011` : "";
|
|
262
|
+
const costSuffix = tool.cost?.text ? `\uFF08cost: ${tool.cost.text}\uFF09` : "";
|
|
263
|
+
const suffix = isWrite ? "\uFF08\u5199\u64CD\u4F5C\uFF0C\u5916\u90E8\u8C03\u7528\u5C06\u4EE5 apikey \u8EAB\u4EFD\u76F4\u63A5\u6267\u884C\uFF09" : "";
|
|
264
|
+
server.registerTool(
|
|
265
|
+
tool.name,
|
|
266
|
+
{
|
|
267
|
+
title,
|
|
268
|
+
description: `${prefix}${tool.description ?? ""}${costSuffix}${suffix}`,
|
|
269
|
+
inputSchema: jsonSchemaToZod(tool.inputSchema),
|
|
270
|
+
annotations: {
|
|
271
|
+
title,
|
|
272
|
+
readOnlyHint: !isWrite,
|
|
273
|
+
destructiveHint: isWrite
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
async (args) => safeRun(async () => {
|
|
277
|
+
const res = await fetch(`${base}/api/mcp/platform/${encodeURIComponent(tool.name)}`, {
|
|
278
|
+
method: "POST",
|
|
279
|
+
headers,
|
|
280
|
+
body: JSON.stringify({ args: args ?? {} }),
|
|
281
|
+
signal: AbortSignal.timeout(12e4)
|
|
282
|
+
});
|
|
283
|
+
const text = await res.text();
|
|
284
|
+
let json;
|
|
285
|
+
try {
|
|
286
|
+
json = JSON.parse(text);
|
|
287
|
+
} catch {
|
|
288
|
+
json = text;
|
|
289
|
+
}
|
|
290
|
+
if (!res.ok) {
|
|
291
|
+
throw new Error(`platform tool failed (HTTP ${res.status}): ${text.slice(0, 500)}`);
|
|
292
|
+
}
|
|
293
|
+
return json;
|
|
294
|
+
})
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
return tools.length;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/server.ts
|
|
301
|
+
var DEFAULT_PORT = 3002;
|
|
302
|
+
var MAX_BODY_BYTES = 4 * 1024 * 1024;
|
|
303
|
+
function configForCredential(credential) {
|
|
304
|
+
const base = loadConfig();
|
|
305
|
+
switch (credential.type) {
|
|
306
|
+
case "bearer":
|
|
307
|
+
return { ...base, apiKey: void 0, accessToken: credential.accessToken };
|
|
308
|
+
case "apikey":
|
|
309
|
+
return { ...base, apiKey: credential.apiKey, accessToken: void 0 };
|
|
310
|
+
default:
|
|
311
|
+
return { ...base, apiKey: void 0, accessToken: void 0 };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async function createMcpServer(config = loadConfig()) {
|
|
315
|
+
const server = new McpServer({
|
|
316
|
+
name: "onv-mcp-server",
|
|
317
|
+
version: "1.0.0"
|
|
318
|
+
});
|
|
319
|
+
const count = await registerTools(server, config);
|
|
320
|
+
console.error(
|
|
321
|
+
`[onv-mcp] registered ${count} platform tools (baseUrl=${config.baseUrl}, auth=${config.accessToken ? "bearer" : config.apiKey ? "apikey" : "anonymous"})`
|
|
322
|
+
);
|
|
323
|
+
return server;
|
|
324
|
+
}
|
|
325
|
+
async function readJsonBody(req) {
|
|
326
|
+
const chunks = [];
|
|
327
|
+
let size = 0;
|
|
328
|
+
for await (const chunk of req) {
|
|
329
|
+
const buf = chunk;
|
|
330
|
+
size += buf.length;
|
|
331
|
+
if (size > MAX_BODY_BYTES) throw new Error("request body too large");
|
|
332
|
+
chunks.push(buf);
|
|
333
|
+
}
|
|
334
|
+
if (size === 0) return void 0;
|
|
335
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
336
|
+
}
|
|
337
|
+
function sendJson(res, status, body) {
|
|
338
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
339
|
+
res.end(JSON.stringify(body));
|
|
340
|
+
}
|
|
341
|
+
function applyCors(res) {
|
|
342
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
343
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
344
|
+
res.setHeader(
|
|
345
|
+
"Access-Control-Allow-Headers",
|
|
346
|
+
"Content-Type, Authorization, X-Api-Key, Mcp-Session-Id, MCP-Protocol-Version"
|
|
347
|
+
);
|
|
348
|
+
res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
|
|
349
|
+
}
|
|
350
|
+
async function startHttpServer(port) {
|
|
351
|
+
const sseTransports = /* @__PURE__ */ new Map();
|
|
352
|
+
const streamTransports = /* @__PURE__ */ new Map();
|
|
353
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
354
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
355
|
+
if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/health")) {
|
|
356
|
+
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
|
|
357
|
+
res.end(
|
|
358
|
+
"ONV MCP Server is running.\nStreamable HTTP: POST/GET/DELETE /mcp\nSSE (legacy): GET /sse + POST /message\n"
|
|
359
|
+
);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (req.method === "GET" && (url.pathname === "/.well-known/oauth-protected-resource" || url.pathname === "/.well-known/oauth-protected-resource/mcp")) {
|
|
363
|
+
res.writeHead(200, {
|
|
364
|
+
"Content-Type": "application/json",
|
|
365
|
+
"Cache-Control": "public, max-age=300"
|
|
366
|
+
});
|
|
367
|
+
res.end(JSON.stringify(protectedResourceMetadata()));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (url.pathname === "/mcp") {
|
|
371
|
+
applyCors(res);
|
|
372
|
+
if (req.method === "OPTIONS") {
|
|
373
|
+
res.writeHead(204).end();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (oauthChallengeEnabled() && !hasCredential(req.headers)) {
|
|
377
|
+
sendOAuthChallenge(res);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
await handleStreamableHttp(req, res, streamTransports);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (req.method === "GET" && url.pathname === "/sse") {
|
|
384
|
+
if (oauthChallengeEnabled() && !hasCredential(req.headers)) {
|
|
385
|
+
sendOAuthChallenge(res);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const messageEndpoint = process.env.MCP_MESSAGE_PATH || "/message";
|
|
389
|
+
const transport = new SSEServerTransport(messageEndpoint, res);
|
|
390
|
+
sseTransports.set(transport.sessionId, transport);
|
|
391
|
+
res.on("close", () => {
|
|
392
|
+
sseTransports.delete(transport.sessionId);
|
|
393
|
+
});
|
|
394
|
+
try {
|
|
395
|
+
const credential = resolveCredential(req.headers);
|
|
396
|
+
const sessionServer = await createMcpServer(configForCredential(credential));
|
|
397
|
+
await sessionServer.connect(transport);
|
|
398
|
+
} catch (err) {
|
|
399
|
+
sseTransports.delete(transport.sessionId);
|
|
400
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
401
|
+
console.error(`[onv-mcp] session ${transport.sessionId} connect failed: ${message}`);
|
|
402
|
+
res.destroy();
|
|
403
|
+
}
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (req.method === "POST" && url.pathname === "/message") {
|
|
407
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
408
|
+
const transport = sessionId ? sseTransports.get(sessionId) : void 0;
|
|
409
|
+
if (!transport) {
|
|
410
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
411
|
+
res.end("No active SSE transport for sessionId");
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
await transport.handlePostMessage(req, res);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
418
|
+
res.end("Not Found");
|
|
419
|
+
});
|
|
420
|
+
await new Promise((resolve) => httpServer.listen(port, resolve));
|
|
421
|
+
return httpServer;
|
|
422
|
+
}
|
|
423
|
+
async function handleStreamableHttp(req, res, transports) {
|
|
424
|
+
const rawSessionId = req.headers["mcp-session-id"];
|
|
425
|
+
const sessionId = Array.isArray(rawSessionId) ? rawSessionId[0] : rawSessionId;
|
|
426
|
+
try {
|
|
427
|
+
let transport = sessionId ? transports.get(sessionId) : void 0;
|
|
428
|
+
if (!transport) {
|
|
429
|
+
if (sessionId) {
|
|
430
|
+
sendJson(res, 404, {
|
|
431
|
+
jsonrpc: "2.0",
|
|
432
|
+
error: { code: -32001, message: "Session not found" },
|
|
433
|
+
id: null
|
|
434
|
+
});
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (req.method !== "POST") {
|
|
438
|
+
sendJson(res, 400, {
|
|
439
|
+
jsonrpc: "2.0",
|
|
440
|
+
error: { code: -32e3, message: "Bad Request: Mcp-Session-Id header is required" },
|
|
441
|
+
id: null
|
|
442
|
+
});
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const body = await readJsonBody(req);
|
|
446
|
+
if (!body || body.method !== "initialize") {
|
|
447
|
+
sendJson(res, 400, {
|
|
448
|
+
jsonrpc: "2.0",
|
|
449
|
+
error: { code: -32e3, message: "Bad Request: first request must be initialize" },
|
|
450
|
+
id: null
|
|
451
|
+
});
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const credential = resolveCredential(req.headers);
|
|
455
|
+
const created = new StreamableHTTPServerTransport({
|
|
456
|
+
sessionIdGenerator: () => randomUUID(),
|
|
457
|
+
onsessioninitialized: (id) => {
|
|
458
|
+
transports.set(id, created);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
created.onclose = () => {
|
|
462
|
+
if (created.sessionId) transports.delete(created.sessionId);
|
|
463
|
+
};
|
|
464
|
+
const sessionServer = await createMcpServer(configForCredential(credential));
|
|
465
|
+
await sessionServer.connect(created);
|
|
466
|
+
await created.handleRequest(req, res, body);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (req.method === "POST") {
|
|
470
|
+
const body = await readJsonBody(req);
|
|
471
|
+
await transport.handleRequest(req, res, body);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
await transport.handleRequest(req, res);
|
|
475
|
+
} catch (err) {
|
|
476
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
477
|
+
console.error(`[onv-mcp] streamable http error: ${message}`);
|
|
478
|
+
if (!res.headersSent) {
|
|
479
|
+
sendJson(res, 500, {
|
|
480
|
+
jsonrpc: "2.0",
|
|
481
|
+
error: { code: -32603, message: "Internal server error" },
|
|
482
|
+
id: null
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async function main() {
|
|
488
|
+
const httpMode = process.argv.includes("--sse") || process.argv.includes("--http");
|
|
489
|
+
if (httpMode) {
|
|
490
|
+
const port = Number(process.env.PORT ?? DEFAULT_PORT);
|
|
491
|
+
await startHttpServer(port);
|
|
492
|
+
console.error(`[onv-mcp] HTTP server listening on http://localhost:${port} (mcp + sse)`);
|
|
493
|
+
} else {
|
|
494
|
+
const server = await createMcpServer();
|
|
495
|
+
const transport = new StdioServerTransport();
|
|
496
|
+
await server.connect(transport);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
main().catch((err) => {
|
|
500
|
+
const message = err instanceof ONVError ? err.message : err instanceof Error ? err.message : String(err);
|
|
501
|
+
console.error(`[onv-mcp] failed to start: ${message}`);
|
|
502
|
+
process.exit(1);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
export { createMcpServer, startHttpServer };
|
|
506
|
+
//# sourceMappingURL=server.js.map
|
|
507
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../packages/sdk-ts/src/errors.ts","../src/auth.ts","../src/oauth.ts","../src/config.ts","../src/tools.ts","../src/server.ts"],"names":[],"mappings":";;;;;;;;;;AAWA,IAAM,WAAA,GAAsC;EAC1C,GAAA,EAAK,iBAAA;EACL,GAAA,EAAK,cAAA;EACL,GAAA,EAAK,WAAA;EACL,GAAA,EAAK,WAAA;EACL,GAAA,EAAK,oBAAA;EACL,GAAA,EAAK,UAAA;EACL,GAAA,EAAK,sBAAA;EACL,GAAA,EAAK,cAAA;EACL,GAAA,EAAK,gBAAA;EACL,GAAA,EAAK,aAAA;EACL,GAAA,EAAK,qBAAA;EACL,GAAA,EAAK;AACP,CAAA;AAEA,IAAM,cAAA,GAAyC;EAC7C,GAAA,EAAK,aAAA;EACL,GAAA,EAAK,cAAA;EACL,GAAA,EAAK,WAAA;EACL,GAAA,EAAK,WAAA;EACL,GAAA,EAAK,oBAAA;EACL,GAAA,EAAK,UAAA;EACL,GAAA,EAAK,sBAAA;EACL,GAAA,EAAK,mBAAA;EACL,GAAA,EAAK,uBAAA;EACL,GAAA,EAAK,aAAA;EACL,GAAA,EAAK,qBAAA;EACL,GAAA,EAAK;AACP,CAAA;AAEA,SAAS,gBAAgB,KAAA,EAAwB;AAC/C,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,EAAU,OAAO,KAAA;AACtC,EAAA,IAAI,KAAA,KAAU,MAAA,IAAa,KAAA,KAAU,IAAA,EAAM,OAAO,EAAA;AAClD,EAAA,IAAI;AACF,IAAA,OAAO,IAAA,CAAK,UAAU,KAAK,CAAA;EAC7B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,OAAO,KAAK,CAAA;AACrB,EAAA;AACF;AAEO,IAAM,QAAA,GAAN,MAAM,SAAA,SAAiB,KAAA,CAAM;;AAEzB,EAAA,IAAA;;AAEA,EAAA,UAAA;;AAEA,EAAA,MAAA;;AAEA,EAAA,MAAA;AAET,EAAA,WAAA,CAAY,OAAA,EAAiB,IAAA,EAAc,UAAA,EAAoB,MAAA,GAAS,IAAI,MAAA,EAAiB;AAC3F,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,UAAA;AACZ,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AACZ,IAAA,IAAA,CAAK,UAAA,GAAa,UAAA;AAClB,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AACd,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AAChB,EAAA;;AAGA,EAAA,IAAI,cAAA,GAA0B;AAC5B,IAAA,OAAO,KAAK,IAAA,KAAS,eAAA;AACvB,EAAA;;;;;;EAOA,OAAO,gBAAA,CAAiB,QAAgB,IAAA,EAAyB;AAC/D,IAAA,MAAM,SAAU,IAAA,IAAQ,OAAO,IAAA,KAAS,QAAA,GAAW,OAAO,EAAA;AAE1D,IAAA,MAAM,UAAU,MAAA,CAAO,IAAA;AACvB,IAAA,MAAM,IAAA,GACJ,OAAO,OAAA,KAAY,QAAA,IAAY,OAAO,OAAA,KAAY,QAAA,GAC9C,MAAA,CAAO,OAAO,CAAA,GACb,WAAA,CAAY,MAAM,CAAA,IAAK,QAAQ,MAAM,CAAA,CAAA;AAE5C,IAAA,MAAM,MAAA,GAAS,eAAA,CAAgB,MAAA,CAAO,MAAM,CAAA;AAC5C,IAAA,MAAM,SAAS,OAAO,MAAA,CAAO,MAAA,KAAW,QAAA,GAAW,OAAO,MAAA,GAAS,MAAA;AAEnE,IAAA,MAAM,OAAA,GACJ,OAAO,MAAA,CAAO,OAAA,KAAY,YAAY,MAAA,CAAO,OAAA,GACzC,MAAA,CAAO,OAAA,GACP,MAAA,GACE,CAAA,KAAA,EAAQ,MAAM,CAAA,EAAA,EAAK,MAAM,CAAA,CAAA,GACzB,CAAA,KAAA,EAAQ,MAAM,CAAA,CAAA,EAAI,eAAe,MAAM,CAAA,IAAK,EAAE,CAAA,CAAA,CAAG,IAAA,EAAA;AAEzD,IAAA,OAAO,IAAI,SAAA,CAAS,OAAA,EAAS,IAAA,EAAM,MAAA,EAAQ,QAAQ,MAAM,CAAA;AAC3D,EAAA;;AAGA,EAAA,OAAO,aAAa,QAAA,EAA8B;AAChD,IAAA,MAAM,KAAA,GAAQ,oBAAoB,KAAA,GAAQ,QAAA,GAAW,IAAI,KAAA,CAAM,MAAA,CAAO,QAAQ,CAAC,CAAA;AAC/E,IAAA,OAAO,IAAI,SAAA;AACT,MAAA,CAAA,eAAA,EAAkB,MAAM,OAAO,CAAA,CAAA;AAC/B,MAAA,eAAA;AACA,MAAA,CAAA;MACA,KAAA,CAAM;AAAA,KAAA;AAEV,EAAA;AACF,CAAA;;;AC9FA,SAAS,YAAY,KAAA,EAA8C;AACjE,EAAA,IAAI,KAAA,CAAM,QAAQ,KAAK,CAAA,SAAU,KAAA,CAAM,CAAC,CAAA,EAAG,IAAA,EAAK,IAAK,EAAA;AACrD,EAAA,OAAA,CAAQ,KAAA,IAAS,IAAI,IAAA,EAAK;AAC5B;AAGO,SAAS,kBAAkB,OAAA,EAA0C;AAC1E,EAAA,MAAM,aAAA,GAAgB,WAAA,CAAY,OAAA,CAAQ,aAAa,CAAA;AACvD,EAAA,IAAI,aAAA,EAAe;AACjB,IAAA,MAAM,OAAA,GAAU,kBAAA,CAAmB,IAAA,CAAK,aAAa,CAAA;AACrD,IAAA,IAAI,OAAA,GAAU,CAAC,CAAA,EAAG,IAAA,EAAK,EAAG;AACxB,MAAA,OAAO,EAAE,MAAM,QAAA,EAAU,WAAA,EAAa,QAAQ,CAAC,CAAA,CAAE,MAAK,EAAE;AAAA,IAC1D;AAAA,EACF;AAEA,EAAA,MAAM,MAAA,GAAS,WAAA,CAAY,OAAA,CAAQ,WAAW,CAAC,CAAA;AAC/C,EAAA,IAAI,MAAA,EAAQ,OAAO,EAAE,IAAA,EAAM,UAAU,MAAA,EAAO;AAE5C,EAAA,OAAO,EAAE,MAAM,WAAA,EAAY;AAC7B;;;ACnBO,SAAS,WAAA,GAAsB;AACpC,EAAA,OAAO,MAAA,CAAO,QAAQ,GAAA,CAAI,gBAAA,IAAoB,mBAAmB,CAAA,CAAE,OAAA,CAAQ,QAAQ,EAAE,CAAA;AACvF;AAGO,SAAS,kBAAA,GAA6B;AAC3C,EAAA,OAAO,MAAA,CAAO,OAAA,CAAQ,GAAA,CAAI,gBAAA,IAAoB,CAAA,EAAG,WAAA,EAAa,CAAA,IAAA,CAAM,CAAA,CAAE,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA;AAC1F;AAGO,SAAS,4BAAA,GAAuC;AACrD,EAAA,OAAO,CAAA,EAAG,aAAa,CAAA,qCAAA,CAAA;AACzB;AAGO,SAAS,yBAAA,GAAqD;AACnE,EAAA,OAAO;AAAA,IACL,UAAU,kBAAA,EAAmB;AAAA,IAC7B,qBAAA,EAAuB,CAAC,WAAA,EAAa,CAAA;AAAA,IACrC,wBAAA,EAA0B,CAAC,QAAQ,CAAA;AAAA,IACnC,gBAAA,EAAkB,CAAC,UAAA,EAAY,WAAA,EAAa,aAAa,cAAc,CAAA;AAAA,IACvE,aAAA,EAAe,kBAAA;AAAA,IACf,sBAAA,EAAwB,CAAA,EAAG,WAAA,EAAa,CAAA,UAAA;AAAA,GAC1C;AACF;AAGO,SAAS,qBAAA,GAAiC;AAC/C,EAAA,OAAO,OAAO,OAAA,CAAQ,GAAA,CAAI,uBAAuB,EAAE,CAAA,CAAE,aAAY,KAAM,MAAA;AACzE;AAGO,SAAS,cAAc,OAAA,EAAuC;AACnE,EAAA,MAAM,OAAO,OAAA,CAAQ,aAAA;AACrB,EAAA,MAAM,MAAA,GAAS,QAAQ,WAAW,CAAA;AAClC,EAAA,OAAO,SAAS,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAA,GAAI,KAAK,CAAC,CAAA,GAAI,IAAA,MAAU,KAAA,CAAM,QAAQ,MAAM,CAAA,GAAI,MAAA,CAAO,CAAC,IAAI,MAAA,CAAO,CAAA;AACvG;AAGO,SAAS,mBAAmB,GAAA,EAG1B;AACP,EAAA,GAAA,CAAI,UAAU,GAAA,EAAK;AAAA,IACjB,cAAA,EAAgB,kBAAA;AAAA,IAChB,kBAAA,EAAoB,CAAA,0BAAA,EAA6B,4BAAA,EAA8B,CAAA,CAAA;AAAA,GAChF,CAAA;AACD,EAAA,GAAA,CAAI,GAAA,CAAI,KAAK,SAAA,CAAU;AAAA,IACrB,OAAA,EAAS,KAAA;AAAA,IACT,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,2CAAA,EAA4C;AAAA,IAC5E,EAAA,EAAI;AAAA,GACL,CAAC,CAAA;AACJ;;;ACrDO,SAAS,UAAA,CAAW,GAAA,GAAyB,OAAA,CAAQ,GAAA,EAAgB;AAC1E,EAAA,OAAO;AAAA,IACL,MAAA,EAAQ,IAAI,WAAA,IAAe,MAAA;AAAA,IAC3B,OAAA,EAAS,IAAI,YAAA,IAAgB;AAAA,GAC/B;AACF;ACqBO,SAAS,gBAAgB,MAAA,EAA2E;AACzG,EAAA,MAAM,QAAsC,EAAC;AAC7C,EAAA,IAAI,CAAC,MAAA,IAAU,OAAO,MAAA,KAAW,UAAU,OAAO,KAAA;AAClD,EAAA,MAAM,KAAA,GAAU,MAAA,CAAe,UAAA,IAAc,EAAC;AAC9C,EAAA,MAAM,QAAA,GAAqB,MAAM,OAAA,CAAS,MAAA,CAAe,QAAQ,CAAA,GAAK,MAAA,CAAe,WAAW,EAAC;AACjG,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,CAAC,KAAK,MAAA,CAAO,OAAA,CAAQ,KAAK,CAAA,EAAG;AAC5C,IAAA,MAAM,IAAA,GAAQ,GAAG,WAAA,IAAe,EAAA;AAChC,IAAA,IAAI,EAAA;AACJ,IAAA,QAAQ,GAAG,IAAA;AAAM,MACf,KAAK,QAAA;AAAU,QAAA,EAAA,GAAK,EAAE,MAAA,EAAO;AAAG,QAAA;AAAA,MAChC,KAAK,SAAA;AAAW,QAAA,EAAA,GAAK,EAAE,OAAA,EAAQ;AAAG,QAAA;AAAA,MAClC,KAAK,OAAA;AAAS,QAAA,EAAA,GAAK,CAAA,CAAE,KAAA,CAAM,CAAA,CAAE,GAAA,EAAK,CAAA;AAAG,QAAA;AAAA,MACrC,KAAK,QAAA;AAAU,QAAA,EAAA,GAAK,EAAE,MAAA,CAAO,CAAA,CAAE,QAAO,EAAG,CAAA,CAAE,KAAK,CAAA;AAAG,QAAA;AAAA,MACnD;AAAS,QAAA,EAAA,GAAK,EAAE,MAAA,EAAO;AAAG,QAAA;AAAA;AAE5B,IAAA,IAAI,QAAsB,QAAA,CAAS,QAAA,CAAS,GAAG,CAAA,GAAI,EAAA,GAAK,GAAG,QAAA,EAAS;AACpE,IAAA,IAAI,IAAA,EAAM,KAAA,GAAQ,KAAA,CAAM,QAAA,CAAS,IAAI,CAAA;AACrC,IAAA,KAAA,CAAM,GAAG,CAAA,GAAI,KAAA;AAAA,EACf;AACA,EAAA,OAAO,KAAA;AACT;AAUA,eAAsB,QAAQ,EAAA,EAA2D;AACvF,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,MAAM,EAAA,EAAG;AACxB,IAAA,MAAM,IAAA,GAAO,WAAW,KAAA,CAAA,GAAY,IAAA,GAAO,KAAK,SAAA,CAAU,MAAA,EAAQ,MAAM,CAAC,CAAA;AACzE,IAAA,OAAO,EAAE,SAAS,CAAC,EAAE,MAAM,MAAA,EAAQ,IAAA,EAAM,CAAA,EAAE;AAAA,EAC7C,SAAS,GAAA,EAAU;AACjB,IAAA,OAAO;AAAA,MACL,OAAA,EAAS;AAAA,QACP;AAAA,UACE,IAAA,EAAM,MAAA;AAAA,UACN,MAAM,IAAA,CAAK,SAAA;AAAA,YACT,EAAE,KAAA,EAAO,EAAE,IAAA,EAAM,aAAA,EAAe,OAAA,EAAS,GAAA,EAAK,OAAA,IAAW,MAAA,CAAO,GAAG,CAAA,EAAE,EAAE;AAAA,YACvE,IAAA;AAAA,YACA;AAAA;AACF;AACF,OACF;AAAA,MACA,OAAA,EAAS;AAAA,KACX;AAAA,EACF;AACF;AAGA,eAAsB,cAAc,MAAA,EAA0D;AAC5F,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,CAAA,EAAG,MAAA,CAAO,QAAQ,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAC,CAAA,uBAAA,CAAA,EAA2B;AAAA,MACtF,MAAA,EAAQ,WAAA,CAAY,OAAA,CAAQ,IAAK;AAAA,KAClC,CAAA;AACD,IAAA,IAAI,CAAC,GAAA,CAAI,EAAA,EAAI,OAAO,IAAA;AACpB,IAAA,MAAM,OAAO,MAAM,GAAA,CAAI,MAAK,CAAE,KAAA,CAAM,MAAM,IAAI,CAAA;AAC9C,IAAA,OAAO,IAAA,EAAM,MAAM,KAAA,IAAS,IAAA;AAAA,EAC9B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAGA,IAAM,YAAA,GAAuC;AAAA,EAC3C,MAAA,EAAQ,cAAA;AAAA,EACR,QAAA,EAAU,cAAA;AAAA,EACV,SAAA,EAAW,gCAAA;AAAA,EACX,OAAA,EAAS;AACX,CAAA;AAGA,SAAS,iBAAiB,IAAA,EAAsB;AAC9C,EAAA,OAAO,KAAK,OAAA,CAAQ,YAAA,EAAc,EAAE,CAAA,CAAE,OAAA,CAAQ,MAAM,GAAG,CAAA;AACzD;AAOA,eAAsB,aAAA,CAAc,QAAmB,MAAA,EAAyC;AAC9F,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,OAAA,CAAQ,OAAA,CAAQ,QAAQ,EAAE,CAAA;AAC9C,EAAA,MAAM,OAAA,GAAkC,EAAE,cAAA,EAAgB,kBAAA,EAAmB;AAC7E,EAAA,IAAI,OAAO,WAAA,EAAa,OAAA,CAAQ,eAAe,CAAA,GAAI,CAAA,OAAA,EAAU,OAAO,WAAW,CAAA,CAAA;AAAA,OAAA,IACtE,MAAA,CAAO,MAAA,EAAQ,OAAA,CAAQ,WAAW,IAAI,MAAA,CAAO,MAAA;AAEtD,EAAA,MAAM,KAAA,GAAQ,MAAM,aAAA,CAAc,MAAM,CAAA;AACxC,EAAA,IAAI,CAAC,KAAA,IAAS,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG;AAEhC,IAAA,MAAA,CAAO,YAAA;AAAA,MACL,mBAAA;AAAA,MACA;AAAA,QACE,KAAA,EAAO,iBAAA;AAAA,QACP,WAAA,EAAa,kLAAA;AAAA,QACb,aAAa;AAAC,OAChB;AAAA,MACA,YACE,QAAQ,OAAO;AAAA,QACb,EAAA,EAAI,KAAA;AAAA,QACJ,KAAA,EAAO,4BAAA;AAAA,QACP,MAAA,EAAQ,KAAA,KAAU,IAAA,GACd,4HAAA,GACA,2HAAA;AAAA,QACJ,OAAA,EAAS,IAAA;AAAA,QACT,SAAA,EAAW,CAAC,CAAC,MAAA,CAAO;AAAA,OACtB,CAAE;AAAA,KACN;AACA,IAAA,OAAO,CAAA;AAAA,EACT;AAEA,EAAA,KAAA,MAAW,QAAQ,KAAA,EAA2B;AAC5C,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,IAAS,gBAAA,CAAiB,KAAK,IAAI,CAAA;AACtD,IAAA,MAAM,OAAA,GAAU,KAAK,IAAA,KAAS,OAAA;AAC9B,IAAA,MAAM,aAAa,IAAA,CAAK,KAAA,GAAQ,YAAA,CAAa,IAAA,CAAK,KAAK,CAAA,GAAI,EAAA;AAC3D,IAAA,MAAM,MAAA,GAAS,UAAA,GAAa,CAAA,MAAA,EAAI,UAAU,CAAA,MAAA,CAAA,GAAM,EAAA;AAChD,IAAA,MAAM,UAAA,GAAa,KAAK,IAAA,EAAM,IAAA,GAAO,eAAU,IAAA,CAAK,IAAA,CAAK,IAAI,CAAA,MAAA,CAAA,GAAM,EAAA;AACnE,IAAA,MAAM,MAAA,GAAS,UAAU,sHAAA,GAA+B,EAAA;AACxD,IAAA,MAAA,CAAO,YAAA;AAAA,MACL,IAAA,CAAK,IAAA;AAAA,MACL;AAAA,QACE,KAAA;AAAA,QACA,WAAA,EAAa,CAAA,EAAG,MAAM,CAAA,EAAG,IAAA,CAAK,eAAe,EAAE,CAAA,EAAG,UAAU,CAAA,EAAG,MAAM,CAAA,CAAA;AAAA,QACrE,WAAA,EAAa,eAAA,CAAgB,IAAA,CAAK,WAAW,CAAA;AAAA,QAC7C,WAAA,EAAa;AAAA,UACX,KAAA;AAAA,UACA,cAAc,CAAC,OAAA;AAAA,UACf,eAAA,EAAiB;AAAA;AACnB,OACF;AAAA,MACA,OAAO,IAAA,KACL,OAAA,CAAQ,YAAY;AAClB,QAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,CAAA,EAAG,IAAI,qBAAqB,kBAAA,CAAmB,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA,EAAI;AAAA,UACnF,MAAA,EAAQ,MAAA;AAAA,UACR,OAAA;AAAA,UACA,IAAA,EAAM,KAAK,SAAA,CAAU,EAAE,MAAM,IAAA,IAAQ,IAAI,CAAA;AAAA,UACzC,MAAA,EAAQ,WAAA,CAAY,OAAA,CAAQ,IAAM;AAAA,SACnC,CAAA;AACD,QAAA,MAAM,IAAA,GAAO,MAAM,GAAA,CAAI,IAAA,EAAK;AAC5B,QAAA,IAAI,IAAA;AACJ,QAAA,IAAI;AACF,UAAA,IAAA,GAAO,IAAA,CAAK,MAAM,IAAI,CAAA;AAAA,QACxB,CAAA,CAAA,MAAQ;AACN,UAAA,IAAA,GAAO,IAAA;AAAA,QACT;AACA,QAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACX,UAAA,MAAM,IAAI,KAAA,CAAM,CAAA,2BAAA,EAA8B,GAAA,CAAI,MAAM,CAAA,GAAA,EAAM,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,GAAG,CAAC,CAAA,CAAE,CAAA;AAAA,QACpF;AACA,QAAA,OAAO,IAAA;AAAA,MACT,CAAC;AAAA,KACL;AAAA,EACF;AACA,EAAA,OAAO,KAAA,CAAM,MAAA;AACf;;;ACxKA,IAAM,YAAA,GAAe,IAAA;AACrB,IAAM,cAAA,GAAiB,IAAI,IAAA,GAAO,IAAA;AAGlC,SAAS,oBAAoB,UAAA,EAAmC;AAC9D,EAAA,MAAM,OAAO,UAAA,EAAW;AACxB,EAAA,QAAQ,WAAW,IAAA;AAAM,IACvB,KAAK,QAAA;AACH,MAAA,OAAO,EAAE,GAAG,IAAA,EAAM,QAAQ,MAAA,EAAW,WAAA,EAAa,WAAW,WAAA,EAAY;AAAA,IAC3E,KAAK,QAAA;AACH,MAAA,OAAO,EAAE,GAAG,IAAA,EAAM,QAAQ,UAAA,CAAW,MAAA,EAAQ,aAAa,MAAA,EAAU;AAAA,IACtE;AACE,MAAA,OAAO,EAAE,GAAG,IAAA,EAAM,MAAA,EAAQ,MAAA,EAAW,aAAa,MAAA,EAAU;AAAA;AAElE;AAMA,eAAsB,eAAA,CAAgB,MAAA,GAAoB,UAAA,EAAW,EAAuB;AAC1F,EAAA,MAAM,MAAA,GAAS,IAAI,SAAA,CAAU;AAAA,IAC3B,IAAA,EAAM,gBAAA;AAAA,IACN,OAAA,EAAS;AAAA,GACV,CAAA;AACD,EAAA,MAAM,KAAA,GAAQ,MAAM,aAAA,CAAc,MAAA,EAAQ,MAAM,CAAA;AAChD,EAAA,OAAA,CAAQ,KAAA;AAAA,IACN,CAAA,qBAAA,EAAwB,KAAK,CAAA,yBAAA,EAA4B,MAAA,CAAO,OAAO,CAAA,OAAA,EACrE,MAAA,CAAO,WAAA,GAAc,QAAA,GAAW,MAAA,CAAO,MAAA,GAAS,QAAA,GAAW,WAC7D,CAAA,CAAA;AAAA,GACF;AACA,EAAA,OAAO,MAAA;AACT;AAGA,eAAe,aAAa,GAAA,EAA6C;AACvE,EAAA,MAAM,SAAmB,EAAC;AAC1B,EAAA,IAAI,IAAA,GAAO,CAAA;AACX,EAAA,WAAA,MAAiB,SAAS,GAAA,EAAK;AAC7B,IAAA,MAAM,GAAA,GAAM,KAAA;AACZ,IAAA,IAAA,IAAQ,GAAA,CAAI,MAAA;AACZ,IAAA,IAAI,IAAA,GAAO,cAAA,EAAgB,MAAM,IAAI,MAAM,wBAAwB,CAAA;AACnE,IAAA,MAAA,CAAO,KAAK,GAAG,CAAA;AAAA,EACjB;AACA,EAAA,IAAI,IAAA,KAAS,GAAG,OAAO,MAAA;AACvB,EAAA,OAAO,IAAA,CAAK,MAAM,MAAA,CAAO,MAAA,CAAO,MAAM,CAAA,CAAE,QAAA,CAAS,MAAM,CAAC,CAAA;AAC1D;AAEA,SAAS,QAAA,CAAS,GAAA,EAA0B,MAAA,EAAgB,IAAA,EAAqB;AAC/E,EAAA,GAAA,CAAI,SAAA,CAAU,MAAA,EAAQ,EAAE,cAAA,EAAgB,oBAAoB,CAAA;AAC5D,EAAA,GAAA,CAAI,GAAA,CAAI,IAAA,CAAK,SAAA,CAAU,IAAI,CAAC,CAAA;AAC9B;AAGA,SAAS,UAAU,GAAA,EAAgC;AACjD,EAAA,GAAA,CAAI,SAAA,CAAU,+BAA+B,GAAG,CAAA;AAChD,EAAA,GAAA,CAAI,SAAA,CAAU,gCAAgC,4BAA4B,CAAA;AAC1E,EAAA,GAAA,CAAI,SAAA;AAAA,IACF,8BAAA;AAAA,IACA;AAAA,GACF;AACA,EAAA,GAAA,CAAI,SAAA,CAAU,iCAAiC,gBAAgB,CAAA;AACjE;AAQA,eAAsB,gBAAgB,IAAA,EAAoC;AACxE,EAAA,MAAM,aAAA,uBAAoB,GAAA,EAAgC;AAC1D,EAAA,MAAM,gBAAA,uBAAuB,GAAA,EAA2C;AAExE,EAAA,MAAM,UAAA,GAAa,IAAA,CAAK,YAAA,CAAa,OAAO,KAAK,GAAA,KAAQ;AACvD,IAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,GAAA,CAAI,GAAA,IAAO,GAAA,EAAK,CAAA,OAAA,EAAU,GAAA,CAAI,OAAA,CAAQ,IAAA,IAAQ,WAAW,CAAA,CAAE,CAAA;AAG/E,IAAA,IAAI,GAAA,CAAI,WAAW,KAAA,KAAU,GAAA,CAAI,aAAa,GAAA,IAAO,GAAA,CAAI,aAAa,SAAA,CAAA,EAAY;AAChF,MAAA,GAAA,CAAI,SAAA,CAAU,GAAA,EAAK,EAAE,cAAA,EAAgB,6BAA6B,CAAA;AAClE,MAAA,GAAA,CAAI,GAAA;AAAA,QACF;AAAA,OAGF;AACA,MAAA;AAAA,IACF;AAIA,IAAA,IAAI,GAAA,CAAI,WAAW,KAAA,KACd,GAAA,CAAI,aAAa,uCAAA,IACjB,GAAA,CAAI,aAAa,2CAAA,CAAA,EAA8C;AAClE,MAAA,GAAA,CAAI,UAAU,GAAA,EAAK;AAAA,QACjB,cAAA,EAAgB,kBAAA;AAAA,QAChB,eAAA,EAAiB;AAAA,OAClB,CAAA;AACD,MAAA,GAAA,CAAI,GAAA,CAAI,IAAA,CAAK,SAAA,CAAU,yBAAA,EAA2B,CAAC,CAAA;AACnD,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,GAAA,CAAI,aAAa,MAAA,EAAQ;AAC3B,MAAA,SAAA,CAAU,GAAG,CAAA;AACb,MAAA,IAAI,GAAA,CAAI,WAAW,SAAA,EAAW;AAC5B,QAAA,GAAA,CAAI,SAAA,CAAU,GAAG,CAAA,CAAE,GAAA,EAAI;AACvB,QAAA;AAAA,MACF;AAEA,MAAA,IAAI,uBAAsB,IAAK,CAAC,aAAA,CAAc,GAAA,CAAI,OAAO,CAAA,EAAG;AAC1D,QAAA,kBAAA,CAAmB,GAAG,CAAA;AACtB,QAAA;AAAA,MACF;AACA,MAAA,MAAM,oBAAA,CAAqB,GAAA,EAAK,GAAA,EAAK,gBAAgB,CAAA;AACrD,MAAA;AAAA,IACF;AAGA,IAAA,IAAI,GAAA,CAAI,MAAA,KAAW,KAAA,IAAS,GAAA,CAAI,aAAa,MAAA,EAAQ;AACnD,MAAA,IAAI,uBAAsB,IAAK,CAAC,aAAA,CAAc,GAAA,CAAI,OAAO,CAAA,EAAG;AAC1D,QAAA,kBAAA,CAAmB,GAAG,CAAA;AACtB,QAAA;AAAA,MACF;AACA,MAAA,MAAM,eAAA,GAAkB,OAAA,CAAQ,GAAA,CAAI,gBAAA,IAAoB,UAAA;AACxD,MAAA,MAAM,SAAA,GAAY,IAAI,kBAAA,CAAmB,eAAA,EAAiB,GAAG,CAAA;AAC7D,MAAA,aAAA,CAAc,GAAA,CAAI,SAAA,CAAU,SAAA,EAAW,SAAS,CAAA;AAChD,MAAA,GAAA,CAAI,EAAA,CAAG,SAAS,MAAM;AACpB,QAAA,aAAA,CAAc,MAAA,CAAO,UAAU,SAAS,CAAA;AAAA,MAC1C,CAAC,CAAA;AACD,MAAA,IAAI;AACF,QAAA,MAAM,UAAA,GAAa,iBAAA,CAAkB,GAAA,CAAI,OAAO,CAAA;AAChD,QAAA,MAAM,aAAA,GAAgB,MAAM,eAAA,CAAgB,mBAAA,CAAoB,UAAU,CAAC,CAAA;AAC3E,QAAA,MAAM,aAAA,CAAc,QAAQ,SAAS,CAAA;AAAA,MACvC,SAAS,GAAA,EAAc;AACrB,QAAA,aAAA,CAAc,MAAA,CAAO,UAAU,SAAS,CAAA;AACxC,QAAA,MAAM,UAAU,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC/D,QAAA,OAAA,CAAQ,MAAM,CAAA,kBAAA,EAAqB,SAAA,CAAU,SAAS,CAAA,iBAAA,EAAoB,OAAO,CAAA,CAAE,CAAA;AACnF,QAAA,GAAA,CAAI,OAAA,EAAQ;AAAA,MACd;AACA,MAAA;AAAA,IACF;AAGA,IAAA,IAAI,GAAA,CAAI,MAAA,KAAW,MAAA,IAAU,GAAA,CAAI,aAAa,UAAA,EAAY;AACxD,MAAA,MAAM,SAAA,GAAY,GAAA,CAAI,YAAA,CAAa,GAAA,CAAI,WAAW,CAAA;AAClD,MAAA,MAAM,SAAA,GAAY,SAAA,GAAY,aAAA,CAAc,GAAA,CAAI,SAAS,CAAA,GAAI,MAAA;AAC7D,MAAA,IAAI,CAAC,SAAA,EAAW;AACd,QAAA,GAAA,CAAI,SAAA,CAAU,GAAA,EAAK,EAAE,cAAA,EAAgB,cAAc,CAAA;AACnD,QAAA,GAAA,CAAI,IAAI,uCAAuC,CAAA;AAC/C,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,SAAA,CAAU,iBAAA,CAAkB,GAAA,EAAK,GAAG,CAAA;AAC1C,MAAA;AAAA,IACF;AAEA,IAAA,GAAA,CAAI,SAAA,CAAU,GAAA,EAAK,EAAE,cAAA,EAAgB,cAAc,CAAA;AACnD,IAAA,GAAA,CAAI,IAAI,WAAW,CAAA;AAAA,EACrB,CAAC,CAAA;AAED,EAAA,MAAM,IAAI,QAAc,CAAC,OAAA,KAAY,WAAW,MAAA,CAAO,IAAA,EAAM,OAAO,CAAC,CAAA;AACrE,EAAA,OAAO,UAAA;AACT;AAGA,eAAe,oBAAA,CACb,GAAA,EACA,GAAA,EACA,UAAA,EACe;AACf,EAAA,MAAM,YAAA,GAAe,GAAA,CAAI,OAAA,CAAQ,gBAAgB,CAAA;AACjD,EAAA,MAAM,YAAY,KAAA,CAAM,OAAA,CAAQ,YAAY,CAAA,GAAI,YAAA,CAAa,CAAC,CAAA,GAAI,YAAA;AAElE,EAAA,IAAI;AACF,IAAA,IAAI,SAAA,GAAY,SAAA,GAAY,UAAA,CAAW,GAAA,CAAI,SAAS,CAAA,GAAI,KAAA,CAAA;AAExD,IAAA,IAAI,CAAC,SAAA,EAAW;AAEd,MAAA,IAAI,SAAA,EAAW;AACb,QAAA,QAAA,CAAS,KAAK,GAAA,EAAK;AAAA,UACjB,OAAA,EAAS,KAAA;AAAA,UACT,KAAA,EAAO,EAAE,IAAA,EAAM,CAAA,KAAA,EAAQ,SAAS,mBAAA,EAAoB;AAAA,UACpD,EAAA,EAAI;AAAA,SACL,CAAA;AACD,QAAA;AAAA,MACF;AAEA,MAAA,IAAI,GAAA,CAAI,WAAW,MAAA,EAAQ;AACzB,QAAA,QAAA,CAAS,KAAK,GAAA,EAAK;AAAA,UACjB,OAAA,EAAS,KAAA;AAAA,UACT,KAAA,EAAO,EAAE,IAAA,EAAM,CAAA,IAAA,EAAQ,SAAS,gDAAA,EAAiD;AAAA,UACjF,EAAA,EAAI;AAAA,SACL,CAAA;AACD,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,IAAA,GAAO,MAAM,YAAA,CAAa,GAAG,CAAA;AACnC,MAAA,IAAI,CAAC,IAAA,IAAS,IAAA,CAA6B,MAAA,KAAW,YAAA,EAAc;AAClE,QAAA,QAAA,CAAS,KAAK,GAAA,EAAK;AAAA,UACjB,OAAA,EAAS,KAAA;AAAA,UACT,KAAA,EAAO,EAAE,IAAA,EAAM,CAAA,IAAA,EAAQ,SAAS,+CAAA,EAAgD;AAAA,UAChF,EAAA,EAAI;AAAA,SACL,CAAA;AACD,QAAA;AAAA,MACF;AAGA,MAAA,MAAM,UAAA,GAAa,iBAAA,CAAkB,GAAA,CAAI,OAAO,CAAA;AAChD,MAAA,MAAM,OAAA,GAAU,IAAI,6BAAA,CAA8B;AAAA,QAChD,kBAAA,EAAoB,MAAM,UAAA,EAAW;AAAA,QACrC,oBAAA,EAAsB,CAAC,EAAA,KAAe;AACpC,UAAA,UAAA,CAAW,GAAA,CAAI,IAAI,OAAO,CAAA;AAAA,QAC5B;AAAA,OACD,CAAA;AACD,MAAA,OAAA,CAAQ,UAAU,MAAM;AACtB,QAAA,IAAI,OAAA,CAAQ,SAAA,EAAW,UAAA,CAAW,MAAA,CAAO,QAAQ,SAAS,CAAA;AAAA,MAC5D,CAAA;AACA,MAAA,MAAM,aAAA,GAAgB,MAAM,eAAA,CAAgB,mBAAA,CAAoB,UAAU,CAAC,CAAA;AAC3E,MAAA,MAAM,aAAA,CAAc,QAAQ,OAAO,CAAA;AACnC,MAAA,MAAM,OAAA,CAAQ,aAAA,CAAc,GAAA,EAAK,GAAA,EAAK,IAAI,CAAA;AAC1C,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,GAAA,CAAI,WAAW,MAAA,EAAQ;AACzB,MAAA,MAAM,IAAA,GAAO,MAAM,YAAA,CAAa,GAAG,CAAA;AACnC,MAAA,MAAM,SAAA,CAAU,aAAA,CAAc,GAAA,EAAK,GAAA,EAAK,IAAI,CAAA;AAC5C,MAAA;AAAA,IACF;AAGA,IAAA,MAAM,SAAA,CAAU,aAAA,CAAc,GAAA,EAAK,GAAG,CAAA;AAAA,EACxC,SAAS,GAAA,EAAc;AACrB,IAAA,MAAM,UAAU,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC/D,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,iCAAA,EAAoC,OAAO,CAAA,CAAE,CAAA;AAC3D,IAAA,IAAI,CAAC,IAAI,WAAA,EAAa;AACpB,MAAA,QAAA,CAAS,KAAK,GAAA,EAAK;AAAA,QACjB,OAAA,EAAS,KAAA;AAAA,QACT,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,uBAAA,EAAwB;AAAA,QACxD,EAAA,EAAI;AAAA,OACL,CAAA;AAAA,IACH;AAAA,EACF;AACF;AAEA,eAAe,IAAA,GAAsB;AACnC,EAAA,MAAM,QAAA,GAAW,QAAQ,IAAA,CAAK,QAAA,CAAS,OAAO,CAAA,IAAK,OAAA,CAAQ,IAAA,CAAK,QAAA,CAAS,QAAQ,CAAA;AAEjF,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,MAAM,IAAA,GAAO,MAAA,CAAO,OAAA,CAAQ,GAAA,CAAI,QAAQ,YAAY,CAAA;AACpD,IAAA,MAAM,gBAAgB,IAAI,CAAA;AAE1B,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,oDAAA,EAAuD,IAAI,CAAA,YAAA,CAAc,CAAA;AAAA,EACzF,CAAA,MAAO;AACL,IAAA,MAAM,MAAA,GAAS,MAAM,eAAA,EAAgB;AACrC,IAAA,MAAM,SAAA,GAAY,IAAI,oBAAA,EAAqB;AAC3C,IAAA,MAAM,MAAA,CAAO,QAAQ,SAAS,CAAA;AAAA,EAChC;AACF;AAEA,IAAA,EAAK,CAAE,KAAA,CAAM,CAAC,GAAA,KAAiB;AAC7B,EAAA,MAAM,OAAA,GAAU,GAAA,YAAe,QAAA,GAAW,GAAA,CAAI,OAAA,GAAU,eAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,MAAA,CAAO,GAAG,CAAA;AACvG,EAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,2BAAA,EAA8B,OAAO,CAAA,CAAE,CAAA;AACrD,EAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAChB,CAAC,CAAA","file":"server.js","sourcesContent":["/**\n * ONVError —— 所有失败(HTTP 错误 / 网络错误 / 参数错误)的统一错误类型。\n *\n * 字段:\n * - `code` 错误码(string)。优先取响应体里的 code,否则按 HTTP 状态码映射(如 404 -> NOT_FOUND)。\n * - `statusCode` HTTP 状态码;网络错误为 0。\n * - `detail` 详细说明(string,对象会被序列化)。\n * - `action` 可选的处理建议(来自响应体 action 字段)。\n */\n\n/** 无响应体 code 时,按 HTTP 状态码映射为稳定错误码。 */\nconst STATUS_CODE: Record<number, string> = {\n 400: 'INVALID_REQUEST',\n 401: 'UNAUTHORIZED',\n 403: 'FORBIDDEN',\n 404: 'NOT_FOUND',\n 405: 'METHOD_NOT_ALLOWED',\n 409: 'CONFLICT',\n 422: 'UNPROCESSABLE_ENTITY',\n 429: 'RATE_LIMITED',\n 500: 'INTERNAL_ERROR',\n 502: 'BAD_GATEWAY',\n 503: 'SERVICE_UNAVAILABLE',\n 504: 'GATEWAY_TIMEOUT',\n};\n\nconst STATUS_MESSAGE: Record<number, string> = {\n 400: 'Bad Request',\n 401: 'Unauthorized',\n 403: 'Forbidden',\n 404: 'Not Found',\n 405: 'Method Not Allowed',\n 409: 'Conflict',\n 422: 'Unprocessable Entity',\n 429: 'Too Many Requests',\n 500: 'Internal Server Error',\n 502: 'Bad Gateway',\n 503: 'Service Unavailable',\n 504: 'Gateway Timeout',\n};\n\nfunction stringifyDetail(value: unknown): string {\n if (typeof value === 'string') return value;\n if (value === undefined || value === null) return '';\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\nexport class ONVError extends Error {\n /** 错误码(string)。 */\n readonly code: string;\n /** HTTP 状态码;网络错误为 0。 */\n readonly statusCode: number;\n /** 详细说明。 */\n readonly detail: string;\n /** 可选的处理建议。 */\n readonly action?: string;\n\n constructor(message: string, code: string, statusCode: number, detail = '', action?: string) {\n super(message);\n this.name = 'ONVError';\n this.code = code;\n this.statusCode = statusCode;\n this.detail = detail;\n this.action = action;\n }\n\n /** 是否为网络层错误(fetch 失败 / 超时)。 */\n get isNetworkError(): boolean {\n return this.code === 'NETWORK_ERROR';\n }\n\n /**\n * 从 HTTP 响应构造错误。\n * 响应体按 `{ code, detail, action }` 解析(同时兼容 `{ message }`);\n * 缺少 code 时按 statusCode 映射(如 404 -> NOT_FOUND)。\n */\n static fromHttpResponse(status: number, body: unknown): ONVError {\n const parsed = (body && typeof body === 'object' ? body : {}) as Record<string, unknown>;\n\n const rawCode = parsed.code;\n const code =\n typeof rawCode === 'string' || typeof rawCode === 'number'\n ? String(rawCode)\n : (STATUS_CODE[status] ?? `HTTP_${status}`);\n\n const detail = stringifyDetail(parsed.detail);\n const action = typeof parsed.action === 'string' ? parsed.action : undefined;\n\n const message =\n typeof parsed.message === 'string' && parsed.message\n ? parsed.message\n : detail\n ? `HTTP ${status}: ${detail}`\n : `HTTP ${status} ${STATUS_MESSAGE[status] ?? ''}`.trim();\n\n return new ONVError(message, code, status, detail, action);\n }\n\n /** 网络失败(fetch 抛错 / 超时 / DNS 等)→ code = NETWORK_ERROR, statusCode = 0。 */\n static networkError(original?: unknown): ONVError {\n const cause = original instanceof Error ? original : new Error(String(original));\n return new ONVError(\n `Network error: ${cause.message}`,\n 'NETWORK_ERROR',\n 0,\n cause.message,\n );\n }\n}\n","/**\n * 凭证解析 —— 同一入口、两种凭证、统一主体(TASKLIST §168 GX3)。\n *\n * 优先级:`Authorization: Bearer` > `X-Api-Key` > 匿名。\n * 解析结果按会话绑定(Streamable HTTP session / SSE transport),\n * 每个会话各自向下游平台透传自己的凭证,不再共用同一把服务端 Key。\n *\n * 注:HTTP 入口**不接受**服务端 env Key 兜底 —— 否则任何匿名调用方都会继承\n * 该 Key 的身份(含写工具),等于公开一个真实账户。匿名会话只可使用公开工具。\n */\nimport type { IncomingHttpHeaders } from 'node:http';\n\nexport type Credential =\n | { type: 'bearer'; accessToken: string }\n | { type: 'apikey'; apiKey: string }\n | { type: 'anonymous' };\n\n/** 取单个头(Node 会把重复头合并为数组)。 */\nfunction headerValue(value: string | string[] | undefined): string {\n if (Array.isArray(value)) return value[0]?.trim() ?? '';\n return (value ?? '').trim();\n}\n\n/** 从请求头解析调用方凭证;均未提供时按匿名处理(仅公开工具可用)。 */\nexport function resolveCredential(headers: IncomingHttpHeaders): Credential {\n const authorization = headerValue(headers.authorization);\n if (authorization) {\n const matched = /^Bearer\\s+(.+)$/i.exec(authorization);\n if (matched?.[1]?.trim()) {\n return { type: 'bearer', accessToken: matched[1].trim() };\n }\n }\n\n const apiKey = headerValue(headers['x-api-key']);\n if (apiKey) return { type: 'apikey', apiKey };\n\n return { type: 'anonymous' };\n}\n","/**\n * MCP OAuth 2.1 资源服务器侧(GX8,TASKLIST §168 GX8)。\n *\n * MCP 规范要求资源服务器(本服务 `/mcp`):\n * ① 提供 RFC 9728 受保护资源元数据(PRM),指向授权服务器;\n * ② 未认证时返回 `401` + `WWW-Authenticate: Bearer resource_metadata=\"…\"`,\n * 客户端据此发现授权服务器并走 OAuth 2.1(PKCE + DCR)取令牌。\n *\n * 授权服务器由 onv-backend 承担(见 backend/routes/mcp-oauth.ts),本文件只负责\n * 元数据与质询头,不签发令牌。\n *\n * 开关:`MCP_OAUTH_CHALLENGE=true` 时对**既无 Bearer 也无 X-Api-Key**的请求返回\n * 401(标准行为)。默认关闭 —— 保留匿名/公开工具的既有可用性;把裸 `/mcp` 变成\n * 「必须登录」属于运营决策,不随代码默认生效。\n */\nimport type { IncomingHttpHeaders } from 'node:http';\n\n/** 授权服务器 issuer(与 backend MCP_OAUTH_ISSUER 保持一致)。 */\nexport function oauthIssuer(): string {\n return String(process.env.MCP_OAUTH_ISSUER || 'https://onv.codes').replace(/\\/+$/, '');\n}\n\n/** 受保护资源标识(本服务 `/mcp`)。 */\nexport function resourceIdentifier(): string {\n return String(process.env.MCP_RESOURCE_URL || `${oauthIssuer()}/mcp`).replace(/\\/+$/, '');\n}\n\n/** PRM 的规范地址(同时用于广告与自检)。 */\nexport function protectedResourceMetadataUrl(): string {\n return `${oauthIssuer()}/.well-known/oauth-protected-resource`;\n}\n\n/** RFC 9728 受保护资源元数据。 */\nexport function protectedResourceMetadata(): Record<string, unknown> {\n return {\n resource: resourceIdentifier(),\n authorization_servers: [oauthIssuer()],\n bearer_methods_supported: ['header'],\n scopes_supported: ['mcp:read', 'mcp:write', 'mcp:trade', 'mcp:backtest'],\n resource_name: 'ONV Platform MCP',\n resource_documentation: `${oauthIssuer()}/mcp-setup`,\n };\n}\n\n/** 是否对无凭据请求发 OAuth 质询(默认关闭,见文件头说明)。 */\nexport function oauthChallengeEnabled(): boolean {\n return String(process.env.MCP_OAUTH_CHALLENGE || '').toLowerCase() === 'true';\n}\n\n/** 请求是否已自带凭据(有则不质询,交由下游按身份放行或拒绝)。 */\nexport function hasCredential(headers: IncomingHttpHeaders): boolean {\n const auth = headers.authorization;\n const apiKey = headers['x-api-key'];\n return Boolean((Array.isArray(auth) ? auth[0] : auth) || (Array.isArray(apiKey) ? apiKey[0] : apiKey));\n}\n\n/** 构造 401 质询响应(JSON-RPC 错误体 + WWW-Authenticate)。 */\nexport function sendOAuthChallenge(res: {\n writeHead: (status: number, headers: Record<string, string>) => unknown;\n end: (body?: string) => unknown;\n}): void {\n res.writeHead(401, {\n 'Content-Type': 'application/json',\n 'WWW-Authenticate': `Bearer resource_metadata=\"${protectedResourceMetadataUrl()}\"`,\n });\n res.end(JSON.stringify({\n jsonrpc: '2.0',\n error: { code: -32001, message: 'Unauthorized: OAuth bearer token required' },\n id: null,\n }));\n}\n","/**\n * MCP Server 环境配置 —— 从环境变量加载 ONV API 凭据与服务地址。\n *\n * 环境变量:\n * - `ONV_API_KEY` (必填)ONV 平台 API Key,作为 `X-Api-Key` 请求头。\n * - `ONV_BASE_URL` (可选)平台 API 根地址,默认 `https://onv.codes`。\n */\nimport { ONV, ONVError } from '@onv/sdk';\n\nexport interface McpConfig {\n apiKey?: string;\n /** OAuth 2.1 access token(GX8 预留):存在时优先以 Bearer 下发,不再发送 X-Api-Key。 */\n accessToken?: string;\n baseUrl: string;\n}\n\n/** 从环境变量加载配置(不抛错,缺少 key 时延后到调用工具时报错)。 */\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): McpConfig {\n return {\n apiKey: env.ONV_API_KEY || undefined,\n baseUrl: env.ONV_BASE_URL || 'https://onv.codes',\n };\n}\n\n/** 基于配置创建 SDK 客户端;缺少 API Key 时抛 ONVError。 */\nexport function createClient(config: McpConfig = loadConfig()): ONV {\n if (!config.apiKey) {\n throw new ONVError(\n 'ONV_API_KEY is not set. Set the ONV_API_KEY environment variable to your ONV API key before calling tools.',\n 'MISSING_API_KEY',\n 400,\n );\n }\n return new ONV({ apiKey: config.apiKey, baseUrl: config.baseUrl });\n}\n","/**\n * MCP 工具注册 —— 平台工具薄代理(MCP 全面化)\n *\n * 不再本地定义工具(避免双套定义漂移):启动时从平台\n * GET {baseUrl}/api/mcp/platform/tools\n * 动态发现工具清单(JSON Schema),把每个工具注册为 MCP 工具,\n * 调用时转发到\n * POST {baseUrl}/api/mcp/platform/:tool body { args }\n * 鉴权用 ONV_API_KEY(X-Api-Key;ahk_/ah_ 均支持)。\n *\n * 平台不可达 / 未配置 key:注册一个只读的 __platform_health 工具\n * 报告状态,避免 MCP 客户端看到空工具列表而困惑。\n */\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { z } from 'zod';\n\nexport interface McpProxyConfig {\n apiKey?: string;\n /** OAuth 2.1 access token(优先于 apiKey 下发)。 */\n accessToken?: string;\n baseUrl: string;\n}\n\n/** 平台发现接口返回的工具描述(含 GX2 的 title / group 与 §169.4 的 cost)。 */\ninterface DiscoveredTool {\n name: string;\n title?: string;\n group?: string;\n mode?: 'read' | 'write';\n description?: string;\n inputSchema?: Record<string, unknown>;\n /** §169.4:由平台按 shared.billing_rates 实时生成,如「≈ 27 credits/次(90d·1H)」 */\n cost?: {\n action?: string;\n base?: number;\n ratePer1kBars?: number;\n ratePer1kTokens?: number;\n free?: boolean;\n text?: string;\n };\n}\n\n/** JSON Schema → zod raw shape(MCP SDK registerTool 需要 zod schema) */\nexport function jsonSchemaToZod(schema: Record<string, unknown> | undefined): Record<string, z.ZodTypeAny> {\n const shape: Record<string, z.ZodTypeAny> = {};\n if (!schema || typeof schema !== 'object') return shape;\n const props = ((schema as any).properties ?? {}) as Record<string, any>;\n const required: string[] = Array.isArray((schema as any).required) ? (schema as any).required : [];\n for (const [key, p] of Object.entries(props)) {\n const desc = (p?.description ?? '') as string;\n let zt: z.ZodTypeAny;\n switch (p?.type) {\n case 'number': zt = z.number(); break;\n case 'boolean': zt = z.boolean(); break;\n case 'array': zt = z.array(z.any()); break;\n case 'object': zt = z.record(z.string(), z.any()); break;\n default: zt = z.string(); break;\n }\n let field: z.ZodTypeAny = required.includes(key) ? zt : zt.optional();\n if (desc) field = field.describe(desc);\n shape[key] = field;\n }\n return shape;\n}\n\n/** CallToolResult 的宽松形态(避免对 SDK 内部类型过度耦合)。 */\nexport interface ToolResult {\n content: Array<{ type: 'text'; text: string }>;\n isError?: boolean;\n [key: string]: unknown;\n}\n\n/** 统一执行:成功返回 JSON 文本,失败返回结构化 error content(不抛到协议层)。 */\nexport async function safeRun(fn: () => Promise<unknown> | unknown): Promise<ToolResult> {\n try {\n const result = await fn();\n const text = result === undefined ? 'OK' : JSON.stringify(result, null, 2);\n return { content: [{ type: 'text', text }] };\n } catch (err: any) {\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(\n { error: { code: 'PROXY_ERROR', message: err?.message || String(err) } },\n null,\n 2,\n ),\n },\n ],\n isError: true,\n };\n }\n}\n\n/** 从平台发现工具清单(失败返回 null)。 */\nexport async function discoverTools(config: McpProxyConfig): Promise<DiscoveredTool[] | null> {\n try {\n const res = await fetch(`${config.baseUrl.replace(/\\/+$/, '')}/api/mcp/platform/tools`, {\n signal: AbortSignal.timeout(15000),\n });\n if (!res.ok) return null;\n const json = await res.json().catch(() => null);\n return json?.data?.tools ?? null;\n } catch {\n return null;\n }\n}\n\n/** 工具分组展示名(GX16:41 条工具按 4 组阅读)。 */\nconst GROUP_LABELS: Record<string, string> = {\n market: '行情',\n strategy: '策略',\n portfolio: '持仓与执行',\n account: '账户与额度',\n};\n\n/** 无平台 title 时的兜底展示名。 */\nfunction humanizeToolName(name: string): string {\n return name.replace(/^platform_/, '').replace(/_/g, ' ');\n}\n\n/**\n * 将平台工具动态注册到 MCP Server(薄代理)。\n * 每个工具 handler:POST {base}/api/mcp/platform/{name},body { args },\n * 凭证按会话透传:OAuth Bearer 优先,其次 X-Api-Key(见 auth.ts)。\n */\nexport async function registerTools(server: McpServer, config: McpProxyConfig): Promise<number> {\n const base = config.baseUrl.replace(/\\/+$/, '');\n const headers: Record<string, string> = { 'content-type': 'application/json' };\n if (config.accessToken) headers['authorization'] = `Bearer ${config.accessToken}`;\n else if (config.apiKey) headers['x-api-key'] = config.apiKey;\n\n const tools = await discoverTools(config);\n if (!tools || tools.length === 0) {\n // 平台不可达 / 未配置 → 注册健康工具报告状态\n server.registerTool(\n '__platform_health',\n {\n title: 'Platform Health',\n description: '平台 MCP 端点状态。工具未加载时用于排查:检查 ONV_BASE_URL 与 ONV_API_KEY 配置。',\n inputSchema: {},\n },\n async () =>\n safeRun(() => ({\n ok: false,\n error: 'platform tools unavailable',\n detail: tools === null\n ? '无法从平台获取工具清单(检查 ONV_BASE_URL 与网络)'\n : '平台工具列表为空(检查 ENABLE_PLATFORM_TOOLS / PLATFORM_TOOL_PUBLIC_URL)',\n baseUrl: base,\n hasApiKey: !!config.apiKey,\n })),\n );\n return 0;\n }\n\n for (const tool of tools as DiscoveredTool[]) {\n const title = tool.title || humanizeToolName(tool.name);\n const isWrite = tool.mode === 'write';\n const groupLabel = tool.group ? GROUP_LABELS[tool.group] : '';\n const prefix = groupLabel ? `【${groupLabel}】` : '';\n const costSuffix = tool.cost?.text ? `(cost: ${tool.cost.text})` : '';\n const suffix = isWrite ? '(写操作,外部调用将以 apikey 身份直接执行)' : '';\n server.registerTool(\n tool.name,\n {\n title,\n description: `${prefix}${tool.description ?? ''}${costSuffix}${suffix}`,\n inputSchema: jsonSchemaToZod(tool.inputSchema),\n annotations: {\n title,\n readOnlyHint: !isWrite,\n destructiveHint: isWrite,\n },\n },\n async (args) =>\n safeRun(async () => {\n const res = await fetch(`${base}/api/mcp/platform/${encodeURIComponent(tool.name)}`, {\n method: 'POST',\n headers,\n body: JSON.stringify({ args: args ?? {} }),\n signal: AbortSignal.timeout(120000),\n });\n const text = await res.text();\n let json: unknown;\n try {\n json = JSON.parse(text);\n } catch {\n json = text;\n }\n if (!res.ok) {\n throw new Error(`platform tool failed (HTTP ${res.status}): ${text.slice(0, 500)}`);\n }\n return json;\n }),\n );\n }\n return tools.length;\n}\n","#!/usr/bin/env node\n/**\n * @onvcodes/mcp-server 主入口(bin 指向 dist/server.js)。\n *\n * 三种运行模式:\n * - 默认 stdio:供 Claude Desktop / Cursor 以 stdio 方式拉起;\n * - `--sse` / `--http`:启动 HTTP 服务器(端口取 env PORT,默认 3002),同时提供\n * ① Streamable HTTP:POST/GET/DELETE `/mcp`(主流目录与新版客户端)\n * ② 兼容 SSE:GET `/sse` + POST `/message`(旧版客户端)\n *\n * 凭证(TASKLIST §168 GX3):同一入口、两种凭证、统一主体。\n * `Authorization: Bearer` > `X-Api-Key` > 匿名(HTTP 入口不使用服务端 env Key 兜底);\n * 解析结果按会话绑定,各自向下游平台透传,不再共用同一把服务端 Key。\n */\nimport { ONVError } from '@onv/sdk';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport http from 'node:http';\nimport { randomUUID } from 'node:crypto';\nimport { resolveCredential, type Credential } from './auth.js';\nimport {\n hasCredential,\n oauthChallengeEnabled,\n protectedResourceMetadata,\n sendOAuthChallenge,\n} from './oauth.js';\nimport { loadConfig, type McpConfig } from './config.js';\nimport { registerTools } from './tools.js';\n\nconst DEFAULT_PORT = 3002;\nconst MAX_BODY_BYTES = 4 * 1024 * 1024;\n\n/** 按凭据构造会话级配置:Bearer 优先,其次客户端 Key,匿名则都不带。 */\nfunction configForCredential(credential: Credential): McpConfig {\n const base = loadConfig();\n switch (credential.type) {\n case 'bearer':\n return { ...base, apiKey: undefined, accessToken: credential.accessToken };\n case 'apikey':\n return { ...base, apiKey: credential.apiKey, accessToken: undefined };\n default:\n return { ...base, apiKey: undefined, accessToken: undefined };\n }\n}\n\n/**\n * 构建 MCP Server:从平台动态发现工具并注册为薄代理。\n * 缺少 API Key / 平台不可达也能正常启动(tools/list 为空或仅健康工具)。\n */\nexport async function createMcpServer(config: McpConfig = loadConfig()): Promise<McpServer> {\n const server = new McpServer({\n name: 'onv-mcp-server',\n version: '1.0.0',\n });\n const count = await registerTools(server, config);\n console.error(\n `[onv-mcp] registered ${count} platform tools (baseUrl=${config.baseUrl}, auth=${\n config.accessToken ? 'bearer' : config.apiKey ? 'apikey' : 'anonymous'\n })`,\n );\n return server;\n}\n\n/** 读取并解析 JSON 请求体(Streamable HTTP 需要显式传 parsedBody)。 */\nasync function readJsonBody(req: http.IncomingMessage): Promise<unknown> {\n const chunks: Buffer[] = [];\n let size = 0;\n for await (const chunk of req) {\n const buf = chunk as Buffer;\n size += buf.length;\n if (size > MAX_BODY_BYTES) throw new Error('request body too large');\n chunks.push(buf);\n }\n if (size === 0) return undefined;\n return JSON.parse(Buffer.concat(chunks).toString('utf8'));\n}\n\nfunction sendJson(res: http.ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(body));\n}\n\n/** 浏览器型 MCP 客户端所需的极简 CORS(不携带 Cookie,故不使用 credentials)。 */\nfunction applyCors(res: http.ServerResponse): void {\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');\n res.setHeader(\n 'Access-Control-Allow-Headers',\n 'Content-Type, Authorization, X-Api-Key, Mcp-Session-Id, MCP-Protocol-Version',\n );\n res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');\n}\n\n/**\n * HTTP 模式:同时提供 Streamable HTTP(`/mcp`)与 SSE(`/sse` + `/message`)。\n *\n * ⚠️ 多会话模型:MCP SDK 的 Server 实例同一时刻只能 connect 一个 transport,\n * 因此每个会话都创建独立的 McpServer(工具清单按会话缓存)。\n */\nexport async function startHttpServer(port: number): Promise<http.Server> {\n const sseTransports = new Map<string, SSEServerTransport>();\n const streamTransports = new Map<string, StreamableHTTPServerTransport>();\n\n const httpServer = http.createServer(async (req, res) => {\n const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);\n\n // 健康检查 / 说明页\n if (req.method === 'GET' && (url.pathname === '/' || url.pathname === '/health')) {\n res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end(\n 'ONV MCP Server is running.\\n' +\n 'Streamable HTTP: POST/GET/DELETE /mcp\\n' +\n 'SSE (legacy): GET /sse + POST /message\\n',\n );\n return;\n }\n\n // RFC 9728 受保护资源元数据(GX8):授权服务器发现的入口。\n // 同时接受 `/.well-known/oauth-protected-resource` 与按路径插入的 `…/mcp`。\n if (req.method === 'GET' &&\n (url.pathname === '/.well-known/oauth-protected-resource' ||\n url.pathname === '/.well-known/oauth-protected-resource/mcp')) {\n res.writeHead(200, {\n 'Content-Type': 'application/json',\n 'Cache-Control': 'public, max-age=300',\n });\n res.end(JSON.stringify(protectedResourceMetadata()));\n return;\n }\n\n if (url.pathname === '/mcp') {\n applyCors(res);\n if (req.method === 'OPTIONS') {\n res.writeHead(204).end();\n return;\n }\n // OAuth 质询(MCP_OAUTH_CHALLENGE=true 时生效):无任何凭据 → 401 + WWW-Authenticate\n if (oauthChallengeEnabled() && !hasCredential(req.headers)) {\n sendOAuthChallenge(res);\n return;\n }\n await handleStreamableHttp(req, res, streamTransports);\n return;\n }\n\n // ── 兼容:SSE 事件流(每会话独立 McpServer + SSEServerTransport,一对一)──\n if (req.method === 'GET' && url.pathname === '/sse') {\n if (oauthChallengeEnabled() && !hasCredential(req.headers)) {\n sendOAuthChallenge(res);\n return;\n }\n const messageEndpoint = process.env.MCP_MESSAGE_PATH || '/message';\n const transport = new SSEServerTransport(messageEndpoint, res);\n sseTransports.set(transport.sessionId, transport);\n res.on('close', () => {\n sseTransports.delete(transport.sessionId);\n });\n try {\n const credential = resolveCredential(req.headers);\n const sessionServer = await createMcpServer(configForCredential(credential));\n await sessionServer.connect(transport);\n } catch (err: unknown) {\n sseTransports.delete(transport.sessionId);\n const message = err instanceof Error ? err.message : String(err);\n console.error(`[onv-mcp] session ${transport.sessionId} connect failed: ${message}`);\n res.destroy();\n }\n return;\n }\n\n // 客户端消息投递(带 sessionId 查询参数)\n if (req.method === 'POST' && url.pathname === '/message') {\n const sessionId = url.searchParams.get('sessionId');\n const transport = sessionId ? sseTransports.get(sessionId) : undefined;\n if (!transport) {\n res.writeHead(400, { 'Content-Type': 'text/plain' });\n res.end('No active SSE transport for sessionId');\n return;\n }\n // SDK v1: handlePostMessage(req, res) —— 第三参为 parsedBody,勿传 sessionId(旧版签名已废弃)\n await transport.handlePostMessage(req, res);\n return;\n }\n\n res.writeHead(404, { 'Content-Type': 'text/plain' });\n res.end('Not Found');\n });\n\n await new Promise<void>((resolve) => httpServer.listen(port, resolve));\n return httpServer;\n}\n\n/** Streamable HTTP:初始化建会话、后续请求按 `Mcp-Session-Id` 复用。 */\nasync function handleStreamableHttp(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n transports: Map<string, StreamableHTTPServerTransport>,\n): Promise<void> {\n const rawSessionId = req.headers['mcp-session-id'];\n const sessionId = Array.isArray(rawSessionId) ? rawSessionId[0] : rawSessionId;\n\n try {\n let transport = sessionId ? transports.get(sessionId) : undefined;\n\n if (!transport) {\n // 已有会话 ID 但找不到 → 会话已过期\n if (sessionId) {\n sendJson(res, 404, {\n jsonrpc: '2.0',\n error: { code: -32001, message: 'Session not found' },\n id: null,\n });\n return;\n }\n\n if (req.method !== 'POST') {\n sendJson(res, 400, {\n jsonrpc: '2.0',\n error: { code: -32000, message: 'Bad Request: Mcp-Session-Id header is required' },\n id: null,\n });\n return;\n }\n\n const body = await readJsonBody(req);\n if (!body || (body as { method?: string }).method !== 'initialize') {\n sendJson(res, 400, {\n jsonrpc: '2.0',\n error: { code: -32000, message: 'Bad Request: first request must be initialize' },\n id: null,\n });\n return;\n }\n\n // 仅初始化请求解析凭证:整条会话沿用该身份\n const credential = resolveCredential(req.headers);\n const created = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n onsessioninitialized: (id: string) => {\n transports.set(id, created);\n },\n });\n created.onclose = () => {\n if (created.sessionId) transports.delete(created.sessionId);\n };\n const sessionServer = await createMcpServer(configForCredential(credential));\n await sessionServer.connect(created);\n await created.handleRequest(req, res, body);\n return;\n }\n\n if (req.method === 'POST') {\n const body = await readJsonBody(req);\n await transport.handleRequest(req, res, body);\n return;\n }\n\n // GET(服务端事件流)/ DELETE(结束会话)\n await transport.handleRequest(req, res);\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n console.error(`[onv-mcp] streamable http error: ${message}`);\n if (!res.headersSent) {\n sendJson(res, 500, {\n jsonrpc: '2.0',\n error: { code: -32603, message: 'Internal server error' },\n id: null,\n });\n }\n }\n}\n\nasync function main(): Promise<void> {\n const httpMode = process.argv.includes('--sse') || process.argv.includes('--http');\n\n if (httpMode) {\n const port = Number(process.env.PORT ?? DEFAULT_PORT);\n await startHttpServer(port);\n // 日志走 stderr,避免污染任何 stdout 协议流。\n console.error(`[onv-mcp] HTTP server listening on http://localhost:${port} (mcp + sse)`);\n } else {\n const server = await createMcpServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n }\n}\n\nmain().catch((err: unknown) => {\n const message = err instanceof ONVError ? err.message : err instanceof Error ? err.message : String(err);\n console.error(`[onv-mcp] failed to start: ${message}`);\n process.exit(1);\n});\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onvcodes/mcp-server",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"mcpName": "codes.onv/onv-mcp",
|
|
4
|
+
"version": "1.0.1",
|
|
5
|
+
"description": "ONV 平台的 MCP (Model Context Protocol) Server —— 将 Agent 管理、回测、AI 分析与市场数据能力暴露为 MCP 工具,供 Claude Desktop / Cursor 等 AI Agent 调用",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"onv-mcp": "dist/server.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup",
|
|
18
|
+
"dev": "tsup --watch",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
22
|
+
"start": "node dist/server.js"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"onv",
|
|
26
|
+
"mcp",
|
|
27
|
+
"model-context-protocol",
|
|
28
|
+
"claude-desktop",
|
|
29
|
+
"cursor",
|
|
30
|
+
"agent",
|
|
31
|
+
"backtest",
|
|
32
|
+
"trading",
|
|
33
|
+
"quant"
|
|
34
|
+
],
|
|
35
|
+
"license": "Apache-2.0",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
41
|
+
"zod": "^3.25 || ^4.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@onv/sdk": "file:../packages/sdk-ts",
|
|
45
|
+
"@types/node": "^22.10.0",
|
|
46
|
+
"tsup": "^8.3.0",
|
|
47
|
+
"typescript": "^5.5.0",
|
|
48
|
+
"vitest": "^3.0.0"
|
|
49
|
+
}
|
|
50
|
+
}
|