@fineorg/mcp 1.0.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.
@@ -0,0 +1,2425 @@
1
+ #!/usr/bin/env node
2
+ import { randomUUID, timingSafeEqual } from 'crypto';
3
+ import http from 'http';
4
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
5
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
6
+ import { readFileSync, createWriteStream } from 'fs';
7
+ import path, { join } from 'path';
8
+ import { config as config$1 } from 'dotenv';
9
+ import { z } from 'zod';
10
+ import { readdir, stat, unlink, mkdir } from 'fs/promises';
11
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
+ import axios2 from 'axios';
13
+ import axiosRetry from 'axios-retry';
14
+ import * as lark from '@larksuiteoapi/node-sdk';
15
+
16
+ config$1({ path: path.resolve(process.cwd(), ".env") });
17
+ var packageJsonPath = path.resolve(process.cwd(), "package.json");
18
+ var packageVersion = "0.0.0";
19
+ try {
20
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
21
+ packageVersion = pkg.version || packageVersion;
22
+ } catch {
23
+ }
24
+ var rawConfig = {
25
+ version: packageVersion,
26
+ mcp: {
27
+ authEnabled: process.env.MCP_AUTH_ENABLED !== "false",
28
+ authToken: process.env.MCP_AUTH_TOKEN,
29
+ allowedClients: process.env.MCP_ALLOWED_CLIENTS,
30
+ http: {
31
+ port: Number.parseInt(process.env.MCP_HTTP_PORT || "3040", 10),
32
+ host: process.env.MCP_HTTP_HOST || "127.0.0.1"
33
+ },
34
+ sse: {
35
+ port: Number.parseInt(process.env.MCP_SSE_PORT || "3041", 10),
36
+ host: process.env.MCP_SSE_HOST || "127.0.0.1"
37
+ },
38
+ maxSessions: Number.parseInt(process.env.MCP_MAX_SESSIONS || "100", 10),
39
+ sessionTimeoutMs: Number.parseInt(process.env.MCP_SESSION_TIMEOUT || String(30 * 60 * 1e3), 10),
40
+ maxBodySize: Number.parseInt(process.env.MCP_MAX_BODY_SIZE || String(1 * 1024 * 1024), 10),
41
+ maxDiffSize: Number.parseInt(process.env.MCP_MAX_DIFF_SIZE || String(2 * 1024 * 1024), 10),
42
+ rateLimitWindowMs: Number.parseInt(process.env.MCP_RATE_LIMIT_WINDOW || "60000", 10),
43
+ rateLimitMax: Number.parseInt(process.env.MCP_RATE_LIMIT_MAX || "60", 10),
44
+ trustProxy: process.env.MCP_TRUST_PROXY === "true"
45
+ },
46
+ jira: {
47
+ host: process.env.JIRA_HOST,
48
+ username: process.env.JIRA_USERNAME,
49
+ token: process.env.JIRA_TOKEN
50
+ },
51
+ bitbucket: {
52
+ host: process.env.BITBUCKET_HOST,
53
+ username: process.env.BITBUCKET_USERNAME,
54
+ password: process.env.BITBUCKET_PASSWORD,
55
+ token: process.env.BITBUCKET_TOKEN
56
+ },
57
+ feishuProject: {
58
+ projectKey: process.env.FEISHU_PROJECT_KEY,
59
+ pluginId: process.env.FEISHU_PROJECT_PLUGIN_ID,
60
+ pluginSecret: process.env.FEISHU_PROJECT_PLUGIN_SECRET,
61
+ useVirtualToken: process.env.FEISHU_PROJECT_USE_VIRTUAL_TOKEN === "true",
62
+ userKey: process.env.FEISHU_PROJECT_USER_KEY
63
+ },
64
+ feishuOpen: {
65
+ appId: process.env.FEISHU_OPEN_APP_ID,
66
+ appSecret: process.env.FEISHU_OPEN_APP_SECRET
67
+ },
68
+ confluence: {
69
+ host: process.env.CONFLUENCE_HOST,
70
+ token: process.env.CONFLUENCE_TOKEN
71
+ },
72
+ server: {
73
+ logDir: process.env.LOG_DIR || "./logs",
74
+ logLevel: process.env.LOG_LEVEL || "INFO"
75
+ }
76
+ };
77
+ var configSchema = z.object({
78
+ version: z.string(),
79
+ mcp: z.object({
80
+ authEnabled: z.boolean(),
81
+ authToken: z.string().optional(),
82
+ allowedClients: z.string().optional(),
83
+ http: z.object({
84
+ port: z.number().int().min(1).max(65535),
85
+ host: z.string()
86
+ }),
87
+ sse: z.object({
88
+ port: z.number().int().min(1).max(65535),
89
+ host: z.string()
90
+ }),
91
+ maxSessions: z.number().int().min(1),
92
+ sessionTimeoutMs: z.number().int().min(1),
93
+ maxBodySize: z.number().int().min(1),
94
+ maxDiffSize: z.number().int().min(1),
95
+ rateLimitWindowMs: z.number().int().min(1e3),
96
+ rateLimitMax: z.number().int().min(1),
97
+ trustProxy: z.boolean()
98
+ }),
99
+ jira: z.object({
100
+ host: z.string().optional(),
101
+ username: z.string().optional(),
102
+ token: z.string().optional()
103
+ }),
104
+ bitbucket: z.object({
105
+ host: z.string().optional(),
106
+ username: z.string().optional(),
107
+ password: z.string().optional(),
108
+ token: z.string().optional()
109
+ }),
110
+ feishuProject: z.object({
111
+ projectKey: z.string().optional(),
112
+ pluginId: z.string().optional(),
113
+ pluginSecret: z.string().optional(),
114
+ useVirtualToken: z.boolean(),
115
+ userKey: z.string().optional()
116
+ }),
117
+ feishuOpen: z.object({
118
+ appId: z.string().optional(),
119
+ appSecret: z.string().optional()
120
+ }),
121
+ confluence: z.object({
122
+ host: z.string().optional(),
123
+ token: z.string().optional()
124
+ }),
125
+ server: z.object({
126
+ logDir: z.string(),
127
+ logLevel: z.string()
128
+ })
129
+ });
130
+ var config = configSchema.parse(rawConfig);
131
+ var hasJiraConfig = () => !!(config.jira.host && config.jira.username && config.jira.token);
132
+ var hasBitbucketAuth = () => !!(config.bitbucket.token || config.bitbucket.username && config.bitbucket.password);
133
+ var hasConfluenceConfig = () => !!(config.confluence.host && config.confluence.token);
134
+ var hasFeishuProjectAuth = () => !!(config.feishuProject.pluginId && config.feishuProject.pluginSecret);
135
+ var hasFeishuOpenAuth = () => !!(config.feishuOpen.appId && config.feishuOpen.appSecret);
136
+ function validateConfig() {
137
+ const errors = [];
138
+ const jiraFields = [config.jira.host, config.jira.username, config.jira.token];
139
+ const jiraCount = jiraFields.filter(Boolean).length;
140
+ if (jiraCount > 0 && jiraCount < jiraFields.length) {
141
+ errors.push("Jira \u914D\u7F6E\u4E0D\u5B8C\u6574\uFF1A\u9700\u8981\u540C\u65F6\u914D\u7F6E JIRA_HOST, JIRA_USERNAME, JIRA_TOKEN");
142
+ }
143
+ if (config.bitbucket.host && !hasBitbucketAuth()) {
144
+ errors.push(
145
+ "Bitbucket \u9700\u8981\u914D\u7F6E\u8BA4\u8BC1\u65B9\u5F0F\uFF1ABITBUCKET_TOKEN \u6216 (BITBUCKET_USERNAME + BITBUCKET_PASSWORD)"
146
+ );
147
+ }
148
+ if (config.confluence.host && !config.confluence.token) {
149
+ errors.push(
150
+ "Confluence \u914D\u7F6E\u4E0D\u5B8C\u6574\uFF1A\u9700\u8981\u540C\u65F6\u914D\u7F6E CONFLUENCE_HOST \u548C CONFLUENCE_TOKEN"
151
+ );
152
+ }
153
+ if (config.feishuProject.projectKey) {
154
+ if (!hasFeishuProjectAuth()) {
155
+ errors.push(
156
+ "\u98DE\u4E66\u9879\u76EE\u914D\u7F6E\u4E0D\u5B8C\u6574\uFF1A\u9700\u8981\u914D\u7F6E FEISHU_PROJECT_PLUGIN_ID + FEISHU_PROJECT_PLUGIN_SECRET"
157
+ );
158
+ }
159
+ if (config.feishuProject.useVirtualToken && !config.feishuProject.userKey) {
160
+ errors.push(
161
+ "\u98DE\u4E66\u9879\u76EE\u4F7F\u7528\u865A\u62DF Token (FEISHU_PROJECT_USE_VIRTUAL_TOKEN=true) \u4F46\u672A\u914D\u7F6E FEISHU_PROJECT_USER_KEY"
162
+ );
163
+ }
164
+ }
165
+ const feishuOpenFields = [config.feishuOpen.appId, config.feishuOpen.appSecret];
166
+ const feishuOpenCount = feishuOpenFields.filter(Boolean).length;
167
+ if (feishuOpenCount > 0 && feishuOpenCount < feishuOpenFields.length) {
168
+ errors.push(
169
+ "\u98DE\u4E66\u5F00\u653E\u5E73\u53F0\u914D\u7F6E\u4E0D\u5B8C\u6574\uFF1A\u9700\u8981\u540C\u65F6\u914D\u7F6E FEISHU_OPEN_APP_ID + FEISHU_OPEN_APP_SECRET"
170
+ );
171
+ }
172
+ if (errors.length > 0) {
173
+ throw new Error(`\u914D\u7F6E\u9A8C\u8BC1\u5931\u8D25:
174
+ ${errors.map((e) => ` - ${e}`).join("\n")}`);
175
+ }
176
+ }
177
+ var platformAvailability = {
178
+ bitbucket: Boolean(config.bitbucket.host && hasBitbucketAuth()),
179
+ jira: hasJiraConfig(),
180
+ confluence: hasConfluenceConfig(),
181
+ feishu: Boolean(config.feishuProject.projectKey && hasFeishuProjectAuth()),
182
+ feishuOpen: hasFeishuOpenAuth()
183
+ };
184
+ var LOG_LEVEL_PRIORITY = {
185
+ DEBUG: 0,
186
+ INFO: 1,
187
+ WARN: 2,
188
+ ERROR: 3
189
+ };
190
+ var currentLogLevel = (() => {
191
+ const envLevel = config.server.logLevel?.toUpperCase();
192
+ if (envLevel && envLevel in LOG_LEVEL_PRIORITY) {
193
+ return envLevel;
194
+ }
195
+ return "INFO";
196
+ })();
197
+ var shouldLog = (level) => {
198
+ return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[currentLogLevel];
199
+ };
200
+ var LOG_DIR = config.server.logDir;
201
+ var LOG_RETENTION_DAYS = 90;
202
+ var timestampFormatter = new Intl.DateTimeFormat("zh-CN", {
203
+ timeZone: "Asia/Shanghai",
204
+ year: "numeric",
205
+ month: "2-digit",
206
+ day: "2-digit",
207
+ hour: "2-digit",
208
+ minute: "2-digit",
209
+ second: "2-digit",
210
+ hour12: false
211
+ });
212
+ var dateFormatter = new Intl.DateTimeFormat("zh-CN", {
213
+ timeZone: "Asia/Shanghai",
214
+ year: "numeric",
215
+ month: "2-digit",
216
+ day: "2-digit"
217
+ });
218
+ var formatTimestamp = () => {
219
+ const now = /* @__PURE__ */ new Date();
220
+ const timeString = timestampFormatter.format(now).replace(/\//g, "-");
221
+ const ms = String(now.getMilliseconds()).padStart(3, "0");
222
+ return `${timeString}.${ms}`;
223
+ };
224
+ var getCurrentDateString = () => {
225
+ const now = /* @__PURE__ */ new Date();
226
+ const parts = dateFormatter.formatToParts(now);
227
+ const year = parts.find((p) => p.type === "year")?.value;
228
+ const month = parts.find((p) => p.type === "month")?.value;
229
+ const day = parts.find((p) => p.type === "day")?.value;
230
+ return `${year}-${month}-${day}`;
231
+ };
232
+ var EXACT_SENSITIVE_KEYS = /* @__PURE__ */ new Set([
233
+ "password",
234
+ "token",
235
+ "secret",
236
+ "authorization",
237
+ "cookie",
238
+ "credential",
239
+ "credentials",
240
+ "bearer"
241
+ ]);
242
+ var SENSITIVE_SUFFIXES = [
243
+ "password",
244
+ "token",
245
+ "secret",
246
+ "apikey",
247
+ "api_key",
248
+ "privatekey",
249
+ "private_key",
250
+ "databaseurl",
251
+ "database_url",
252
+ "connectionstring",
253
+ "connection_string"
254
+ ];
255
+ var MAX_SANITIZE_DEPTH = 5;
256
+ var sanitizeSensitiveData = (data, depth = 0) => {
257
+ if (data === null || data === void 0) {
258
+ return data;
259
+ }
260
+ if (depth > MAX_SANITIZE_DEPTH) {
261
+ return "[MAX_DEPTH_REACHED]";
262
+ }
263
+ if (typeof data === "string") {
264
+ if (data.startsWith("Bearer ")) {
265
+ return "Bearer [REDACTED]";
266
+ }
267
+ if (data.startsWith("Basic ")) {
268
+ return "Basic [REDACTED]";
269
+ }
270
+ if (data.includes("://") && data.includes("@")) {
271
+ try {
272
+ const url = new URL(data);
273
+ if (url.password) {
274
+ url.password = "***REDACTED***";
275
+ }
276
+ return url.toString();
277
+ } catch {
278
+ return data.replace(/:([^:@]+)@/, ":***REDACTED***@");
279
+ }
280
+ }
281
+ return data;
282
+ }
283
+ if (Array.isArray(data)) {
284
+ return data.map((item) => sanitizeSensitiveData(item, depth + 1));
285
+ }
286
+ if (typeof data === "object") {
287
+ if (data instanceof Error) {
288
+ return {
289
+ message: sanitizeSensitiveData(data.message, depth + 1),
290
+ stack: data.stack,
291
+ name: data.name
292
+ };
293
+ }
294
+ const sanitized = {};
295
+ for (const [key, value] of Object.entries(data)) {
296
+ const keyLower = key.toLowerCase();
297
+ const isSensitive = EXACT_SENSITIVE_KEYS.has(keyLower) || SENSITIVE_SUFFIXES.some((suffix) => keyLower.endsWith(suffix));
298
+ if (isSensitive) {
299
+ sanitized[key] = "***REDACTED***";
300
+ } else {
301
+ sanitized[key] = sanitizeSensitiveData(value, depth + 1);
302
+ }
303
+ }
304
+ return sanitized;
305
+ }
306
+ return data;
307
+ };
308
+ var ensureLogDir = async () => {
309
+ try {
310
+ await mkdir(LOG_DIR, { recursive: true });
311
+ } catch {
312
+ }
313
+ };
314
+ var logDirReady = false;
315
+ var makeStreamFactory = (prefix) => {
316
+ let stream = null;
317
+ let date = "";
318
+ return (dateStr) => {
319
+ try {
320
+ if (stream && date === dateStr) return stream;
321
+ stream?.end();
322
+ stream = createWriteStream(join(LOG_DIR, `${prefix}-${dateStr}.log`), {
323
+ flags: "a",
324
+ encoding: "utf-8"
325
+ });
326
+ stream.on("error", (err) => {
327
+ console.error(`[logger] write stream error: ${err.message}`);
328
+ });
329
+ date = dateStr;
330
+ return stream;
331
+ } catch {
332
+ return null;
333
+ }
334
+ };
335
+ };
336
+ var getOrCreateLogStream = makeStreamFactory("mcp");
337
+ var getOrCreateErrorStream = makeStreamFactory("error");
338
+ var writeToFile = async (logMessage) => {
339
+ try {
340
+ if (!logDirReady) {
341
+ await ensureLogDir();
342
+ logDirReady = true;
343
+ }
344
+ const dateStr = getCurrentDateString();
345
+ const stream = getOrCreateLogStream(dateStr);
346
+ stream?.write(`${logMessage}
347
+ `);
348
+ } catch {
349
+ }
350
+ };
351
+ var writeToErrorFile = async (logMessage) => {
352
+ try {
353
+ if (!logDirReady) {
354
+ await ensureLogDir();
355
+ logDirReady = true;
356
+ }
357
+ const dateStr = getCurrentDateString();
358
+ const stream = getOrCreateErrorStream(dateStr);
359
+ stream?.write(`${logMessage}
360
+ `);
361
+ } catch {
362
+ }
363
+ };
364
+ var cleanupOldLogs = async () => {
365
+ try {
366
+ await ensureLogDir();
367
+ const files = await readdir(LOG_DIR);
368
+ const now = Date.now();
369
+ const retentionMs = LOG_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
370
+ for (const file of files) {
371
+ if (!file.startsWith("mcp-") && !file.startsWith("error-") || !file.endsWith(".log")) {
372
+ continue;
373
+ }
374
+ const filePath = join(LOG_DIR, file);
375
+ const fileStat = await stat(filePath);
376
+ const fileAge = now - fileStat.mtime.getTime();
377
+ if (fileAge > retentionMs) {
378
+ await unlink(filePath);
379
+ }
380
+ }
381
+ } catch {
382
+ }
383
+ };
384
+ var log = (level, message, data) => {
385
+ if (!shouldLog(level)) {
386
+ return;
387
+ }
388
+ const timestamp = formatTimestamp();
389
+ let logMessage = `[${timestamp}] [${level}] ${message}`;
390
+ if (data !== void 0) {
391
+ try {
392
+ const sanitizedData = sanitizeSensitiveData(data);
393
+ let dataStr = "";
394
+ if (sanitizedData instanceof Error) {
395
+ const errorObj = {
396
+ message: sanitizedData.message,
397
+ stack: sanitizedData.stack?.replace(/\n/g, "\\n"),
398
+ name: sanitizedData.name
399
+ };
400
+ if ("code" in sanitizedData) {
401
+ errorObj.code = sanitizedData.code;
402
+ }
403
+ dataStr = JSON.stringify(errorObj);
404
+ } else if (typeof sanitizedData === "object" && sanitizedData !== null) {
405
+ const dataObj = {};
406
+ for (const [key, value] of Object.entries(sanitizedData)) {
407
+ try {
408
+ if (typeof value === "function" || value === void 0) {
409
+ continue;
410
+ }
411
+ if (value instanceof Error) {
412
+ dataObj[key] = {
413
+ message: value.message,
414
+ stack: value.stack?.replace(/\n/g, "\\n"),
415
+ name: value.name
416
+ };
417
+ } else if (typeof value === "string") {
418
+ dataObj[key] = value.replace(/\n/g, "\\n");
419
+ } else {
420
+ dataObj[key] = value;
421
+ }
422
+ } catch {
423
+ dataObj[key] = `[Unserializable: ${typeof value}]`;
424
+ }
425
+ }
426
+ dataStr = JSON.stringify(dataObj);
427
+ } else {
428
+ dataStr = String(sanitizedData);
429
+ }
430
+ logMessage += ` ${dataStr}`;
431
+ } catch {
432
+ logMessage += ` [Data type: ${typeof data}, Message: ${String(data).substring(0, 100)}]`;
433
+ }
434
+ }
435
+ console.error(logMessage);
436
+ writeToFile(logMessage).catch(() => {
437
+ });
438
+ if (level === "ERROR") {
439
+ writeToErrorFile(logMessage).catch(() => {
440
+ });
441
+ }
442
+ };
443
+ function initLogger() {
444
+ cleanupOldLogs().catch(() => {
445
+ });
446
+ scheduleCleanup();
447
+ }
448
+ var CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1e3;
449
+ var cleanupTimer;
450
+ var scheduleCleanup = () => {
451
+ cleanupTimer = setInterval(() => {
452
+ cleanupOldLogs().catch(() => {
453
+ });
454
+ }, CLEANUP_INTERVAL_MS);
455
+ cleanupTimer.unref();
456
+ };
457
+ var logRequestError = (message, context) => {
458
+ const enhancedData = {
459
+ type: "HTTP_REQUEST_ERROR",
460
+ ...context
461
+ };
462
+ log("ERROR", message, enhancedData);
463
+ };
464
+ var logger = {
465
+ debug: (message, data) => log("DEBUG", message, data),
466
+ info: (message, data) => log("INFO", message, data),
467
+ warn: (message, data) => log("WARN", message, data),
468
+ error: (message, data) => log("ERROR", message, data),
469
+ requestError: logRequestError
470
+ };
471
+
472
+ // src/mcp/mcpAuth.ts
473
+ var cachedAllowedClients;
474
+ var cachedAllowedClientsSource;
475
+ function getParsedAllowedClients() {
476
+ const source = config.mcp.allowedClients;
477
+ if (cachedAllowedClientsSource === source && cachedAllowedClients !== void 0) {
478
+ return cachedAllowedClients;
479
+ }
480
+ cachedAllowedClientsSource = source;
481
+ cachedAllowedClients = source ? source.split(",").map((c) => c.trim().toLowerCase()).filter((c) => c.length > 0) : null;
482
+ return cachedAllowedClients;
483
+ }
484
+ var McpAuthError = class extends Error {
485
+ constructor(message) {
486
+ super(message);
487
+ this.name = "McpAuthError";
488
+ }
489
+ };
490
+ function validateMcpStartup() {
491
+ if (!config.mcp.authEnabled) {
492
+ logger.info("MCP Server \u8BA4\u8BC1\u672A\u542F\u7528\uFF0C\u5141\u8BB8\u6240\u6709\u5BA2\u6237\u7AEF\u8BBF\u95EE");
493
+ return;
494
+ }
495
+ if (!config.mcp.authToken) {
496
+ throw new McpAuthError(
497
+ "MCP_AUTH_TOKEN \u672A\u914D\u7F6E\u3002\u8BF7\u4F7F\u7528 'openssl rand -hex 32' \u751F\u6210\u4EE4\u724C\u5E76\u6DFB\u52A0\u5230 .env \u6587\u4EF6\u4E2D"
498
+ );
499
+ }
500
+ if (config.mcp.authToken.length < 32) {
501
+ throw new McpAuthError(
502
+ `MCP_AUTH_TOKEN \u957F\u5EA6\u4E0D\u8DB3\uFF08\u81F3\u5C11\u9700\u8981 32 \u5B57\u7B26\uFF09\u3002\u8BF7\u4F7F\u7528 'openssl rand -hex 32' \u751F\u6210\u65B0\u4EE4\u724C`
503
+ );
504
+ }
505
+ logger.info("MCP Server \u8BA4\u8BC1\u5DF2\u542F\u7528", {
506
+ hasValidToken: true,
507
+ hasAllowedClients: !!config.mcp.allowedClients
508
+ });
509
+ }
510
+ function validateAuthToken(authToken) {
511
+ if (!config.mcp.authEnabled) {
512
+ return true;
513
+ }
514
+ if (!authToken) {
515
+ logger.warn("MCP \u5DE5\u5177\u8C03\u7528\u672A\u63D0\u4F9B\u8BA4\u8BC1\u4EE4\u724C");
516
+ return false;
517
+ }
518
+ if (!config.mcp.authToken) {
519
+ logger.warn("MCP \u5DE5\u5177\u8C03\u7528\u4EE4\u724C\u9A8C\u8BC1\u5931\u8D25");
520
+ return false;
521
+ }
522
+ const expected = Buffer.from(config.mcp.authToken);
523
+ const received = Buffer.from(authToken);
524
+ if (expected.length !== received.length) {
525
+ logger.warn("MCP \u5DE5\u5177\u8C03\u7528\u4EE4\u724C\u9A8C\u8BC1\u5931\u8D25");
526
+ return false;
527
+ }
528
+ if (!timingSafeEqual(expected, received)) {
529
+ logger.warn("MCP \u5DE5\u5177\u8C03\u7528\u4EE4\u724C\u9A8C\u8BC1\u5931\u8D25");
530
+ return false;
531
+ }
532
+ return true;
533
+ }
534
+ function validateClient(clientName) {
535
+ if (!config.mcp.authEnabled) {
536
+ return true;
537
+ }
538
+ const allowedClients = getParsedAllowedClients();
539
+ if (!allowedClients) {
540
+ return true;
541
+ }
542
+ if (!clientName) {
543
+ logger.warn("MCP \u5DE5\u5177\u8C03\u7528\u672A\u63D0\u4F9B\u5BA2\u6237\u7AEF\u540D\u79F0");
544
+ return false;
545
+ }
546
+ const isAllowed = allowedClients.includes(clientName.toLowerCase());
547
+ if (!isAllowed) {
548
+ logger.warn("MCP \u5BA2\u6237\u7AEF\u4E0D\u5728\u767D\u540D\u5355\u4E2D", {
549
+ clientName
550
+ });
551
+ }
552
+ return isAllowed;
553
+ }
554
+ function validateHttpAuth(req) {
555
+ if (!config.mcp.authEnabled) return true;
556
+ const authHeader = req.headers.authorization;
557
+ const token = typeof authHeader === "string" ? authHeader.replace(/^Bearer\s+/i, "") : void 0;
558
+ if (!validateAuthToken(token)) {
559
+ logger.warn("HTTP \u8FDE\u63A5\u8BA4\u8BC1\u5931\u8D25", {
560
+ ip: req.socket.remoteAddress,
561
+ authHeader: authHeader ? "present" : "missing"
562
+ });
563
+ return false;
564
+ }
565
+ const clientHeader = req.headers["x-mcp-client"];
566
+ const clientName = typeof clientHeader === "string" ? clientHeader : void 0;
567
+ if (!validateClient(clientName)) {
568
+ logger.warn("HTTP \u8FDE\u63A5\u5BA2\u6237\u7AEF\u9A8C\u8BC1\u5931\u8D25", {
569
+ ip: req.socket.remoteAddress,
570
+ clientName: clientName || "missing"
571
+ });
572
+ return false;
573
+ }
574
+ return true;
575
+ }
576
+ function getMcpAuthStatus() {
577
+ return {
578
+ authEnabled: config.mcp.authEnabled,
579
+ hasAuthToken: !!config.mcp.authToken,
580
+ tokenLengthSufficient: (config.mcp.authToken?.length || 0) >= 32,
581
+ hasAllowedClients: !!config.mcp.allowedClients
582
+ };
583
+ }
584
+
585
+ // src/mcp/rate-limiter.ts
586
+ var RateLimiter = class {
587
+ windowMs;
588
+ maxRequests;
589
+ entries = /* @__PURE__ */ new Map();
590
+ cleanupTimer;
591
+ constructor(windowMs, maxRequests) {
592
+ this.windowMs = windowMs;
593
+ this.maxRequests = maxRequests;
594
+ this.cleanupTimer = setInterval(() => this.cleanup(), windowMs * 2);
595
+ this.cleanupTimer.unref();
596
+ }
597
+ isAllowed(key) {
598
+ const now = Date.now();
599
+ const entry = this.entries.get(key);
600
+ if (!entry || now - entry.windowStart >= this.windowMs) {
601
+ this.entries.set(key, { count: 1, windowStart: now });
602
+ return true;
603
+ }
604
+ entry.count++;
605
+ if (entry.count > this.maxRequests) {
606
+ logger.warn("\u901F\u7387\u9650\u5236\u89E6\u53D1", {
607
+ key,
608
+ count: entry.count,
609
+ maxRequests: this.maxRequests,
610
+ windowMs: this.windowMs
611
+ });
612
+ return false;
613
+ }
614
+ return true;
615
+ }
616
+ cleanup() {
617
+ const now = Date.now();
618
+ for (const [key, entry] of this.entries) {
619
+ if (now - entry.windowStart >= this.windowMs) {
620
+ this.entries.delete(key);
621
+ }
622
+ }
623
+ }
624
+ destroy() {
625
+ if (this.cleanupTimer) {
626
+ clearInterval(this.cleanupTimer);
627
+ this.cleanupTimer = void 0;
628
+ }
629
+ this.entries.clear();
630
+ }
631
+ };
632
+
633
+ // src/types/result.ts
634
+ function success(data) {
635
+ return { success: true, data };
636
+ }
637
+ function failure(error) {
638
+ return { success: false, error };
639
+ }
640
+ function extractHttpErrorDetail(error) {
641
+ const detail = {};
642
+ if (!(error instanceof Error)) {
643
+ detail.raw = String(error);
644
+ return detail;
645
+ }
646
+ detail.message = error.message;
647
+ if (axios2.isAxiosError(error)) {
648
+ detail.axiosCode = error.code;
649
+ if (error.config) {
650
+ detail.requestMethod = error.config.method;
651
+ detail.requestUrl = error.config.url;
652
+ detail.requestBaseURL = error.config.baseURL;
653
+ }
654
+ if (error.response) {
655
+ detail.httpStatus = error.response.status;
656
+ detail.httpStatusText = error.response.statusText;
657
+ extractResponseBody(error.response.data, detail);
658
+ }
659
+ return detail;
660
+ }
661
+ const err = error;
662
+ if (err.response && typeof err.response === "object") {
663
+ const resp = err.response;
664
+ detail.httpStatus = resp.status;
665
+ if (resp.data && typeof resp.data === "object") {
666
+ extractResponseBody(resp.data, detail);
667
+ }
668
+ }
669
+ if (err.config && typeof err.config === "object") {
670
+ const cfg = err.config;
671
+ detail.requestUrl = cfg.url;
672
+ detail.requestMethod = cfg.method;
673
+ }
674
+ if (err.code) detail.axiosCode = err.code;
675
+ return detail;
676
+ }
677
+ function extractResponseBody(data, detail) {
678
+ if (typeof data === "string") {
679
+ detail.responseBody = data.slice(0, 500);
680
+ return;
681
+ }
682
+ if (typeof data === "object" && data !== null) {
683
+ const d = data;
684
+ const body = {};
685
+ for (const key of ["message", "error", "errors", "code", "msg", "log_id", "troubleshooter"]) {
686
+ if (d[key] !== void 0) body[key] = d[key];
687
+ }
688
+ if (Object.keys(body).length > 0) {
689
+ detail.responseBody = body;
690
+ }
691
+ }
692
+ }
693
+
694
+ // src/services/platforms/BasePlatformClient.ts
695
+ var RETRY_ERROR_CODES = ["ECONNREFUSED", "ETIMEDOUT", "ENOTFOUND", "ENETUNREACH"];
696
+ var BasePlatformClient = class {
697
+ api;
698
+ platformName;
699
+ constructor(platformName, config2) {
700
+ this.platformName = platformName;
701
+ this.api = this.buildAxiosInstance(config2);
702
+ logger.debug(`${platformName} \u5BA2\u6237\u7AEF\u5DF2\u521D\u59CB\u5316`, { baseURL: config2.baseURL });
703
+ }
704
+ /**
705
+ * 构建 axios 实例(含认证和重试配置)
706
+ */
707
+ buildAxiosInstance(config2) {
708
+ const { baseURL, auth, timeout = 3e4, retries = 3 } = config2;
709
+ const headers = {};
710
+ let axiosAuth;
711
+ if (auth.type === "bearer") {
712
+ headers.Authorization = `Bearer ${auth.token}`;
713
+ } else if (auth.type === "basic") {
714
+ axiosAuth = {
715
+ username: auth.username,
716
+ password: auth.password
717
+ };
718
+ }
719
+ const instance = axios2.create({
720
+ baseURL,
721
+ headers,
722
+ auth: axiosAuth,
723
+ timeout
724
+ });
725
+ this.setupRetry(instance, retries);
726
+ return instance;
727
+ }
728
+ /**
729
+ * 配置自动重试机制
730
+ */
731
+ setupRetry(instance, retries) {
732
+ axiosRetry(instance, {
733
+ retries,
734
+ retryCondition: (error) => {
735
+ return axios2.isAxiosError(error) && (!error.response || error.response.status >= 500 || RETRY_ERROR_CODES.includes(error.code || ""));
736
+ },
737
+ onRetry: (retryCount, error) => {
738
+ logger.warn(`${this.platformName} API \u8BF7\u6C42\u91CD\u8BD5`, {
739
+ retryCount,
740
+ ...extractHttpErrorDetail(error)
741
+ });
742
+ }
743
+ });
744
+ }
745
+ /**
746
+ * 创建额外的 API 实例(共享相同的认证和重试配置)
747
+ */
748
+ createApiInstance(config2) {
749
+ return this.buildAxiosInstance(config2);
750
+ }
751
+ /**
752
+ * 包装 API 调用,统一错误处理
753
+ */
754
+ async wrapApiCall(operation, apiCall) {
755
+ const startTime = Date.now();
756
+ try {
757
+ logger.debug(`${this.platformName}: ${operation}`);
758
+ const data = await apiCall();
759
+ logger.debug(`${this.platformName}: ${operation} \u6210\u529F`, {
760
+ durationMs: Date.now() - startTime
761
+ });
762
+ return success(data);
763
+ } catch (error) {
764
+ const durationMs = Date.now() - startTime;
765
+ const errorMessage = error instanceof Error ? error.message : String(error);
766
+ logger.error(`${this.platformName}: ${operation} \u5931\u8D25`, {
767
+ durationMs,
768
+ ...extractHttpErrorDetail(error)
769
+ });
770
+ return failure(
771
+ new Error(`${this.platformName} ${operation} \u5931\u8D25: ${errorMessage}`)
772
+ );
773
+ }
774
+ }
775
+ /**
776
+ * 获取平台名称
777
+ */
778
+ getPlatformName() {
779
+ return this.platformName;
780
+ }
781
+ };
782
+
783
+ // src/services/platforms/BitbucketClient.ts
784
+ var BitbucketClient = class extends BasePlatformClient {
785
+ /**
786
+ * Patch API 使用独立实例,因为 Bitbucket Server 的 REST API 和 Patch API
787
+ * 使用不同的路径前缀(/rest/api/latest vs /rest/patch/1.0)。
788
+ * 合并为单实例需要在每次调用时传入完整路径,增加出错风险,故保持分离。
789
+ */
790
+ patchApi;
791
+ constructor(config2) {
792
+ if (!config2.baseUrl) {
793
+ throw new Error("Bitbucket baseUrl is required");
794
+ }
795
+ if (!config2.token && !(config2.username && config2.password)) {
796
+ throw new Error("Either token or username/password is required for Bitbucket authentication");
797
+ }
798
+ const auth = config2.token ? { type: "bearer", token: config2.token } : { type: "basic", username: config2.username, password: config2.password };
799
+ super("Bitbucket", {
800
+ baseURL: `${config2.baseUrl}/rest/api/latest`,
801
+ auth,
802
+ timeout: 3e4
803
+ });
804
+ this.patchApi = this.createApiInstance({
805
+ baseURL: `${config2.baseUrl}/rest/patch/1.0`,
806
+ auth,
807
+ timeout: 3e4
808
+ });
809
+ }
810
+ async getPullRequestDiff(params, contextLines = 10) {
811
+ const { project, repository, prId } = params;
812
+ return this.wrapApiCall(`\u83B7\u53D6 PR diff (${project}/${repository}#${prId})`, async () => {
813
+ const response = await this.api.get(
814
+ `/projects/${project}/repos/${repository}/pull-requests/${prId}/diff`,
815
+ {
816
+ params: { contextLines },
817
+ headers: { Accept: "text/plain" }
818
+ }
819
+ );
820
+ return response.data;
821
+ });
822
+ }
823
+ async getPullRequestPatch(params) {
824
+ const { project, repository, prId } = params;
825
+ return this.wrapApiCall(`\u83B7\u53D6 PR patch (${project}/${repository}#${prId})`, async () => {
826
+ const response = await this.patchApi.get(
827
+ `/projects/${project}/repos/${repository}/pull-requests/${prId}/patch`
828
+ );
829
+ return response.data;
830
+ });
831
+ }
832
+ async getPullRequest(params) {
833
+ const { project, repository, prId } = params;
834
+ return this.wrapApiCall(`\u83B7\u53D6 PR \u8BE6\u60C5 (${project}/${repository}#${prId})`, async () => {
835
+ const response = await this.api.get(
836
+ `/projects/${project}/repos/${repository}/pull-requests/${prId}`
837
+ );
838
+ return response.data;
839
+ });
840
+ }
841
+ async getPullRequests(params) {
842
+ const { project, repository, state = "OPEN", limit = 100 } = params;
843
+ return this.wrapApiCall(`\u83B7\u53D6 PR \u5217\u8868 (${project}/${repository})`, async () => {
844
+ const response = await this.api.get(`/projects/${project}/repos/${repository}/pull-requests`, {
845
+ params: { state, limit }
846
+ });
847
+ return response.data.values;
848
+ });
849
+ }
850
+ async getPullRequestChanges(params) {
851
+ const { project, repository, prId } = params;
852
+ return this.wrapApiCall(`\u83B7\u53D6 PR \u53D8\u66F4\u6587\u4EF6 (${project}/${repository}#${prId})`, async () => {
853
+ const response = await this.api.get(
854
+ `/projects/${project}/repos/${repository}/pull-requests/${prId}/changes`,
855
+ {
856
+ params: { limit: 1e3 }
857
+ }
858
+ );
859
+ const files = response.data.values.map((change) => {
860
+ return change.path?.toString || "";
861
+ }).filter(Boolean);
862
+ return files;
863
+ });
864
+ }
865
+ async getPullRequestCommitsCount(params) {
866
+ const { project, repository, prId } = params;
867
+ return this.wrapApiCall(`\u83B7\u53D6 PR \u63D0\u4EA4\u6570\u91CF (${project}/${repository}#${prId})`, async () => {
868
+ const response = await this.api.get(
869
+ `/projects/${project}/repos/${repository}/pull-requests/${prId}/commits`,
870
+ {
871
+ params: { limit: 1 }
872
+ }
873
+ );
874
+ return response.data.size || 0;
875
+ });
876
+ }
877
+ async createInlineComment(project, repository, prId, file, line, text) {
878
+ return this.wrapApiCall(`\u521B\u5EFA\u884C\u5185\u8BC4\u8BBA (${project}/${repository}#${prId})`, async () => {
879
+ const url = `/projects/${project}/repos/${repository}/pull-requests/${prId}/comments`;
880
+ const requestBody = {
881
+ text,
882
+ anchor: {
883
+ diffType: "EFFECTIVE",
884
+ path: file,
885
+ line,
886
+ lineType: "ADDED"
887
+ }
888
+ };
889
+ const response = await this.api.post(url, requestBody);
890
+ return response.status === 201;
891
+ });
892
+ }
893
+ async getPullRequestComments(params) {
894
+ const { project, repository, prId } = params;
895
+ return this.wrapApiCall(`\u83B7\u53D6 PR \u8BC4\u8BBA (${project}/${repository}#${prId})`, async () => {
896
+ const url = `/projects/${project}/repos/${repository}/pull-requests/${prId}/activities`;
897
+ const response = await this.api.get(url, {
898
+ params: { limit: 100 }
899
+ });
900
+ if (response.data && response.data.values) {
901
+ const comments = response.data.values.filter((activity) => activity.action === "COMMENTED" && activity.comment).map((activity) => activity.comment);
902
+ return comments;
903
+ }
904
+ return [];
905
+ });
906
+ }
907
+ async createPullRequest(params) {
908
+ const { title, description, from, to } = params;
909
+ return this.wrapApiCall(`\u521B\u5EFA Pull Request (${to.project}/${to.repository})`, async () => {
910
+ const response = await this.api.post(
911
+ `/projects/${to.project}/repos/${to.repository}/pull-requests`,
912
+ {
913
+ title,
914
+ description: description || "",
915
+ fromRef: {
916
+ id: `refs/heads/${from.branch}`,
917
+ repository: {
918
+ slug: from.repository,
919
+ project: {
920
+ key: from.project
921
+ }
922
+ }
923
+ },
924
+ toRef: {
925
+ id: `refs/heads/${to.branch}`,
926
+ repository: {
927
+ slug: to.repository,
928
+ project: {
929
+ key: to.project
930
+ }
931
+ }
932
+ }
933
+ }
934
+ );
935
+ return response.data;
936
+ });
937
+ }
938
+ };
939
+ function createBitbucketClient() {
940
+ if (!config.bitbucket.host) return null;
941
+ const hasToken = !!config.bitbucket.token;
942
+ const hasBasicAuth = !!(config.bitbucket.username && config.bitbucket.password);
943
+ if (!hasToken && !hasBasicAuth) return null;
944
+ return new BitbucketClient({
945
+ baseUrl: config.bitbucket.host,
946
+ token: config.bitbucket.token,
947
+ username: config.bitbucket.username,
948
+ password: config.bitbucket.password
949
+ });
950
+ }
951
+
952
+ // src/services/platforms/ConfluenceClient.ts
953
+ var ConfluenceClient = class extends BasePlatformClient {
954
+ host;
955
+ constructor(host, token) {
956
+ super("Confluence", {
957
+ baseURL: `${host}/rest/api`,
958
+ auth: { type: "bearer", token },
959
+ timeout: 3e4
960
+ });
961
+ this.host = host;
962
+ this.api.defaults.headers.common["Content-Type"] = "application/json";
963
+ }
964
+ async getPage(pageId) {
965
+ return this.wrapApiCall(`\u83B7\u53D6\u9875\u9762 ${pageId}`, async () => {
966
+ const response = await this.api.get(`/content/${pageId}`, {
967
+ params: {
968
+ expand: "body.storage,version,history,space"
969
+ }
970
+ });
971
+ const page = response.data;
972
+ return {
973
+ id: page.id,
974
+ type: page.type,
975
+ title: page.title,
976
+ space: {
977
+ key: page.space?.key,
978
+ name: page.space?.name
979
+ },
980
+ version: {
981
+ number: page.version?.number,
982
+ when: page.version?.when,
983
+ by: page.version?.by?.displayName
984
+ },
985
+ body: {
986
+ storage: page.body?.storage?.value
987
+ },
988
+ history: {
989
+ createdBy: page.history?.createdBy?.displayName,
990
+ createdDate: page.history?.createdDate
991
+ },
992
+ webUrl: new URL(page._links?.webui ?? "", this.host).toString()
993
+ };
994
+ });
995
+ }
996
+ async getChildPages(pageId, options = {}) {
997
+ const { limit = 50, expand = "version,space" } = options;
998
+ return this.wrapApiCall(`\u83B7\u53D6\u9875\u9762 ${pageId} \u7684\u5B50\u9875\u9762`, async () => {
999
+ const response = await this.api.get(
1000
+ `/content/${pageId}/child/page`,
1001
+ {
1002
+ params: {
1003
+ limit,
1004
+ expand
1005
+ }
1006
+ }
1007
+ );
1008
+ const childPages = response.data.results.map((child) => ({
1009
+ id: child.id,
1010
+ type: child.type,
1011
+ title: child.title,
1012
+ status: child.status,
1013
+ space: {
1014
+ key: child.space?.key,
1015
+ name: child.space?.name
1016
+ },
1017
+ version: {
1018
+ number: child.version?.number,
1019
+ when: child.version?.when,
1020
+ by: child.version?.by?.displayName
1021
+ },
1022
+ webUrl: new URL(child._links?.webui ?? "", this.host).toString(),
1023
+ position: child.extensions?.position
1024
+ }));
1025
+ return {
1026
+ parentPageId: pageId,
1027
+ totalChildren: response.data.size,
1028
+ start: response.data.start,
1029
+ limit: response.data.limit,
1030
+ children: childPages
1031
+ };
1032
+ });
1033
+ }
1034
+ async getPageComments(pageId, options = {}) {
1035
+ const { limit = 50 } = options;
1036
+ return this.wrapApiCall(`\u83B7\u53D6\u9875\u9762 ${pageId} \u7684\u8BC4\u8BBA`, async () => {
1037
+ const response = await this.api.get(
1038
+ `/content/${pageId}/child/comment`,
1039
+ {
1040
+ params: {
1041
+ limit,
1042
+ expand: "body.storage,version,extensions.inlineProperties"
1043
+ }
1044
+ }
1045
+ );
1046
+ const comments = response.data.results.map((comment) => ({
1047
+ id: comment.id,
1048
+ body: comment.body?.storage?.value,
1049
+ author: comment.version?.by?.displayName,
1050
+ createdAt: comment.version?.when,
1051
+ location: comment.extensions?.location
1052
+ }));
1053
+ return {
1054
+ pageId,
1055
+ totalComments: response.data.size,
1056
+ start: response.data.start,
1057
+ limit: response.data.limit,
1058
+ comments
1059
+ };
1060
+ });
1061
+ }
1062
+ };
1063
+ function createConfluenceClient() {
1064
+ const { host, token } = config.confluence;
1065
+ if (!host || !token) return null;
1066
+ return new ConfluenceClient(host, token);
1067
+ }
1068
+ var FEISHU_PREFIX_MAP = {
1069
+ m: "story",
1070
+ f: "issue",
1071
+ g: "assignment"
1072
+ };
1073
+ function parseFeishuWorkItemId(workItemId) {
1074
+ const urlMatch = workItemId.match(/project\.feishu\.cn\/[^/]+\/([^/]+)\/detail\/(\d+)/);
1075
+ if (urlMatch) {
1076
+ return { type: urlMatch[1], id: urlMatch[2] };
1077
+ }
1078
+ const prefixMatch = workItemId.match(/^([a-zA-Z]+)-(\d+)$/);
1079
+ if (prefixMatch) {
1080
+ const prefix = prefixMatch[1].toLowerCase();
1081
+ return { type: FEISHU_PREFIX_MAP[prefix] || prefix, id: prefixMatch[2] };
1082
+ }
1083
+ if (/^\d+$/.test(workItemId)) {
1084
+ return { type: "issue", id: workItemId };
1085
+ }
1086
+ throw new Error(`\u65E0\u6548\u7684\u98DE\u4E66\u5DE5\u4F5C\u9879 ID: ${workItemId.slice(0, 100)}`);
1087
+ }
1088
+ var FeishuClient = class extends BasePlatformClient {
1089
+ projectKey;
1090
+ pluginId;
1091
+ pluginSecret;
1092
+ useVirtualToken;
1093
+ userKey;
1094
+ pluginToken = "";
1095
+ pluginTokenExpireTimestamp = 0;
1096
+ tokenRefreshPromise = null;
1097
+ tokenApi;
1098
+ constructor(cfg = {}) {
1099
+ super("Feishu", {
1100
+ baseURL: "https://project.feishu.cn/open_api",
1101
+ auth: { type: "none" },
1102
+ timeout: 3e4
1103
+ });
1104
+ this.projectKey = cfg.projectKey;
1105
+ this.pluginId = cfg.pluginId;
1106
+ this.pluginSecret = cfg.pluginSecret;
1107
+ this.useVirtualToken = cfg.useVirtualToken ?? false;
1108
+ this.userKey = cfg.userKey;
1109
+ this.tokenApi = axios2.create({ timeout: 3e4 });
1110
+ this.api.interceptors.request.use(async (reqConfig) => {
1111
+ const token = await this.getPluginToken();
1112
+ if (token) {
1113
+ reqConfig.headers["X-PLUGIN-TOKEN"] = token;
1114
+ }
1115
+ if (this.userKey) {
1116
+ reqConfig.headers["X-USER-KEY"] = this.userKey;
1117
+ }
1118
+ reqConfig.headers["Content-Type"] = "application/json; charset=utf-8";
1119
+ return reqConfig;
1120
+ });
1121
+ this.api.interceptors.response.use(
1122
+ (response) => response,
1123
+ async (error) => {
1124
+ const { response, config: reqConfig } = error;
1125
+ if ((response?.status === 401 || response?.status === 403) && reqConfig && !reqConfig._retried) {
1126
+ this.pluginToken = "";
1127
+ this.pluginTokenExpireTimestamp = 0;
1128
+ reqConfig._retried = true;
1129
+ logger.warn("\u98DE\u4E66 API \u8BA4\u8BC1\u5931\u8D25\uFF0C\u5237\u65B0 token \u540E\u91CD\u8BD5", { status: response?.status });
1130
+ return this.api.request(reqConfig);
1131
+ }
1132
+ return Promise.reject(error);
1133
+ }
1134
+ );
1135
+ }
1136
+ async getPluginToken() {
1137
+ if (!this.pluginId || !this.pluginSecret) {
1138
+ logger.warn("\u98DE\u4E66\u9879\u76EE\u8BA4\u8BC1\u51ED\u8BC1\u7F3A\u5931 (\u9700\u8981 Plugin ID/Secret \u6216 User Token)");
1139
+ return "";
1140
+ }
1141
+ if (this.pluginToken && Date.now() < this.pluginTokenExpireTimestamp) {
1142
+ return this.pluginToken;
1143
+ }
1144
+ if (this.tokenRefreshPromise) {
1145
+ return this.tokenRefreshPromise;
1146
+ }
1147
+ this.tokenRefreshPromise = this.refreshPluginToken();
1148
+ try {
1149
+ return await this.tokenRefreshPromise;
1150
+ } finally {
1151
+ this.tokenRefreshPromise = null;
1152
+ }
1153
+ }
1154
+ async refreshPluginToken() {
1155
+ try {
1156
+ const url = "https://project.feishu.cn/open_api/authen/plugin_token";
1157
+ const tokenType = this.useVirtualToken ? 1 : 0;
1158
+ const tokenTypeName = this.useVirtualToken ? "virtual_plugin_token" : "plugin_access_token";
1159
+ logger.debug(`\u8BF7\u6C42\u98DE\u4E66 Plugin Token (type=${tokenType}, ${tokenTypeName})`);
1160
+ const response = await this.tokenApi.post(url, {
1161
+ plugin_id: this.pluginId,
1162
+ plugin_secret: this.pluginSecret,
1163
+ type: tokenType
1164
+ });
1165
+ const { error: errorInfo, data: authData } = response.data;
1166
+ if (errorInfo && errorInfo.code !== 0) {
1167
+ logger.error("\u83B7\u53D6 Plugin Token \u5931\u8D25", {
1168
+ code: errorInfo.code,
1169
+ msg: errorInfo.msg,
1170
+ tokenType: tokenTypeName
1171
+ });
1172
+ return "";
1173
+ }
1174
+ this.pluginToken = authData?.token || "";
1175
+ if (!this.pluginToken) {
1176
+ logger.error("Plugin Token \u4E0D\u5B58\u5728\u4E8E\u54CD\u5E94\u4E2D");
1177
+ return "";
1178
+ }
1179
+ const expiresIn = authData?.expire_time || 7200;
1180
+ this.pluginTokenExpireTimestamp = Date.now() + (expiresIn - 60) * 1e3;
1181
+ logger.info("\u6210\u529F\u83B7\u53D6\u98DE\u4E66\u9879\u76EE Plugin Token", {
1182
+ tokenType: tokenTypeName,
1183
+ expiresIn
1184
+ });
1185
+ return this.pluginToken;
1186
+ } catch (error) {
1187
+ logger.error("\u83B7\u53D6\u98DE\u4E66\u9879\u76EE Plugin Token \u5931\u8D25", { error });
1188
+ return "";
1189
+ }
1190
+ }
1191
+ async getWorkItemComments(workItemTypeKey, workItemId) {
1192
+ if (!this.projectKey || !this.userKey) {
1193
+ return [];
1194
+ }
1195
+ try {
1196
+ logger.debug(`\u5C1D\u8BD5\u83B7\u53D6\u98DE\u4E66\u5DE5\u4F5C\u9879\u8BC4\u8BBA`, {
1197
+ workItemId,
1198
+ workItemTypeKey
1199
+ });
1200
+ const response = await this.api.get(
1201
+ `/${this.projectKey}/work_item/${workItemTypeKey}/${workItemId}/comments`
1202
+ );
1203
+ if (response.data.err_code !== 0) {
1204
+ logger.warn(`\u83B7\u53D6\u98DE\u4E66\u5DE5\u4F5C\u9879\u8BC4\u8BBA\u5931\u8D25`, {
1205
+ err_code: response.data.err_code,
1206
+ err_msg: response.data.err_msg,
1207
+ workItemId
1208
+ });
1209
+ return [];
1210
+ }
1211
+ const comments = response.data.data || [];
1212
+ const { pagination } = response.data;
1213
+ logger.info(`\u6210\u529F\u83B7\u53D6\u98DE\u4E66\u5DE5\u4F5C\u9879\u8BC4\u8BBA`, {
1214
+ workItemId,
1215
+ commentCount: comments.length,
1216
+ total: pagination?.total,
1217
+ pageNum: pagination?.page_num,
1218
+ pageSize: pagination?.page_size
1219
+ });
1220
+ return comments.map((comment) => ({
1221
+ id: String(comment.id),
1222
+ content: comment.content,
1223
+ workItemId: String(comment.work_item_id),
1224
+ workItemTypeKey: comment.work_item_type_key,
1225
+ createdAt: comment.created_at,
1226
+ operator: comment.operator
1227
+ }));
1228
+ } catch (error) {
1229
+ const errorMsg = error instanceof Error ? error.message : String(error);
1230
+ const axiosError = error;
1231
+ logger.error(`\u83B7\u53D6\u98DE\u4E66\u5DE5\u4F5C\u9879\u8BC4\u8BBA\u5931\u8D25 (\u53EF\u80FD\u4E0D\u652F\u6301\u8BE5 API)`, {
1232
+ workItemId,
1233
+ error: errorMsg,
1234
+ status: axiosError.response?.status
1235
+ });
1236
+ return [];
1237
+ }
1238
+ }
1239
+ async getWorkItem(workItemId) {
1240
+ if (!this.projectKey) {
1241
+ return failure(new Error("\u98DE\u4E66\u9879\u76EE\u914D\u7F6E\u7F3A\u5931 (\u9700\u8981 FEISHU_PROJECT_KEY)"));
1242
+ }
1243
+ if (!this.userKey) {
1244
+ return failure(new Error("\u98DE\u4E66\u9879\u76EE\u914D\u7F6E\u7F3A\u5931 (\u9700\u8981 FEISHU_PROJECT_USER_KEY)"));
1245
+ }
1246
+ const { type: workItemTypeKey, id: actualId } = parseFeishuWorkItemId(workItemId);
1247
+ logger.debug("\u89E3\u6790\u98DE\u4E66\u5DE5\u4F5C\u9879 ID", {
1248
+ input: workItemId,
1249
+ type: workItemTypeKey,
1250
+ id: actualId
1251
+ });
1252
+ return this.wrapApiCall(`\u83B7\u53D6\u5DE5\u4F5C\u9879 ${actualId}`, async () => {
1253
+ const response = await this.api.post(
1254
+ `/${this.projectKey}/work_item/${workItemTypeKey}/query`,
1255
+ {
1256
+ work_item_ids: [Number.parseInt(actualId)]
1257
+ }
1258
+ );
1259
+ if (response.data.err_code !== 0) {
1260
+ throw new Error(
1261
+ `\u98DE\u4E66 API \u9519\u8BEF (err_code=${response.data.err_code}): ${response.data.err_msg}`
1262
+ );
1263
+ }
1264
+ const items = response.data.data;
1265
+ if (!items || items.length === 0) {
1266
+ throw new Error(`\u98DE\u4E66\u5DE5\u4F5C\u9879\u4E0D\u5B58\u5728 (${actualId})`);
1267
+ }
1268
+ const workItem = items[0];
1269
+ let description = "";
1270
+ const descField = workItem.fields?.find((f) => f.field_key === "description");
1271
+ if (descField && "field_value" in descField) {
1272
+ description = String(descField.field_value || "");
1273
+ }
1274
+ const comments = await this.getWorkItemComments(workItemTypeKey, actualId);
1275
+ return {
1276
+ projectKey: this.projectKey,
1277
+ id: String(workItem.id),
1278
+ name: workItem.name || "",
1279
+ description,
1280
+ work_item_type_key: workItem.work_item_type_key || workItemTypeKey,
1281
+ work_item_type_name: workItem.work_item_type_name || workItem.work_item_type_key || workItemTypeKey,
1282
+ comments
1283
+ };
1284
+ });
1285
+ }
1286
+ };
1287
+ function createFeishuClient() {
1288
+ const { projectKey, pluginId, pluginSecret } = config.feishuProject;
1289
+ if (!projectKey || !pluginId || !pluginSecret) return null;
1290
+ return new FeishuClient({
1291
+ projectKey,
1292
+ pluginId,
1293
+ pluginSecret,
1294
+ useVirtualToken: config.feishuProject.useVirtualToken,
1295
+ userKey: config.feishuProject.userKey
1296
+ });
1297
+ }
1298
+ var FEISHU_OPEN_DOC_TYPE_MAP = {
1299
+ docx: "docx",
1300
+ doc: "doc",
1301
+ sheet: "sheet",
1302
+ sheets: "sheet",
1303
+ wiki: "docx",
1304
+ base: "file",
1305
+ slides: "slides"
1306
+ };
1307
+ function parseFeishuDocumentId(input) {
1308
+ const urlMatch = input.match(/\.feishu\.cn\/([a-z]+)\/([A-Za-z0-9]+)/);
1309
+ if (urlMatch) {
1310
+ const rawType = urlMatch[1];
1311
+ const fileType = FEISHU_OPEN_DOC_TYPE_MAP[rawType] || "docx";
1312
+ return { documentId: urlMatch[2], fileType, isWiki: rawType === "wiki" };
1313
+ }
1314
+ if (/^[A-Za-z0-9]{6,}$/.test(input)) {
1315
+ return { documentId: input, fileType: "docx", isWiki: false };
1316
+ }
1317
+ throw new Error(`\u65E0\u6548\u7684\u98DE\u4E66\u6587\u6863\u6807\u8BC6: ${input.slice(0, 100)}`);
1318
+ }
1319
+ function extractReplyText(reply) {
1320
+ return (reply.content?.elements || []).filter((el) => el.type === "text_run" && el.text_run?.text).map((el) => el.text_run.text).join("");
1321
+ }
1322
+ var PARAGRAPH_BLOCK_TYPE = 2;
1323
+ var sdkLogger = {
1324
+ error: (...msg) => logger.error("FeishuSDK", { detail: msg.length === 1 ? msg[0] : msg }),
1325
+ warn: (...msg) => logger.warn("FeishuSDK", { detail: msg.length === 1 ? msg[0] : msg }),
1326
+ info: (...msg) => logger.info("FeishuSDK", { detail: msg.length === 1 ? msg[0] : msg }),
1327
+ debug: (...msg) => logger.debug("FeishuSDK", { detail: msg.length === 1 ? msg[0] : msg }),
1328
+ trace: (...msg) => logger.debug("FeishuSDK:trace", { detail: msg.length === 1 ? msg[0] : msg })
1329
+ };
1330
+ var FeishuOpenClient = class {
1331
+ client;
1332
+ constructor(cfg) {
1333
+ this.client = new lark.Client({
1334
+ appId: cfg.appId,
1335
+ appSecret: cfg.appSecret,
1336
+ loggerLevel: lark.LoggerLevel.warn,
1337
+ logger: sdkLogger
1338
+ });
1339
+ logger.info("FeishuOpen \u5BA2\u6237\u7AEF\u5DF2\u521D\u59CB\u5316", { appId: cfg.appId });
1340
+ }
1341
+ /**
1342
+ * 通过 wiki token 获取实际的 document_id 和文档类型
1343
+ * wiki 页面的 token 与 document_id 不同,需要先解析
1344
+ */
1345
+ async resolveWikiToken(wikiToken) {
1346
+ const res = await this.client.wiki.space.getNode({
1347
+ params: { token: wikiToken }
1348
+ });
1349
+ if (res.code !== 0) {
1350
+ throw new Error(`\u89E3\u6790 Wiki token \u5931\u8D25 (code=${res.code}): ${res.msg}`);
1351
+ }
1352
+ const node = res.data?.node;
1353
+ if (!node?.obj_token) {
1354
+ throw new Error("\u89E3\u6790 Wiki token \u6210\u529F\u4F46\u672A\u8FD4\u56DE obj_token");
1355
+ }
1356
+ return { objToken: node.obj_token, objType: node.obj_type ?? "docx" };
1357
+ }
1358
+ async getDocumentContent(documentId) {
1359
+ const res = await this.client.docx.document.rawContent({
1360
+ path: { document_id: documentId }
1361
+ });
1362
+ if (res.code !== 0) {
1363
+ throw new Error(`\u83B7\u53D6\u6587\u6863\u5185\u5BB9\u5931\u8D25 (code=${res.code}): ${res.msg}`);
1364
+ }
1365
+ return res.data?.content ?? "";
1366
+ }
1367
+ async getDocumentComments(fileToken, fileType) {
1368
+ const comments = [];
1369
+ let pageToken;
1370
+ for (let page = 0; page < 5; page++) {
1371
+ try {
1372
+ const res = await this.client.drive.fileComment.list({
1373
+ path: { file_token: fileToken },
1374
+ params: {
1375
+ file_type: fileType,
1376
+ page_size: 50,
1377
+ ...pageToken ? { page_token: pageToken } : {}
1378
+ }
1379
+ });
1380
+ if (res.code !== 0) {
1381
+ logger.warn("\u83B7\u53D6\u6587\u6863\u8BC4\u8BBA\u5931\u8D25", { code: res.code, msg: res.msg, fileToken });
1382
+ break;
1383
+ }
1384
+ for (const item of res.data?.items ?? []) {
1385
+ const replies = item.reply_list?.replies ?? [];
1386
+ const content = replies.map(extractReplyText).filter(Boolean).join("\n");
1387
+ if (content && item.comment_id) {
1388
+ comments.push({
1389
+ commentId: item.comment_id,
1390
+ content,
1391
+ userId: item.user_id ?? "unknown",
1392
+ createTime: item.create_time ?? 0,
1393
+ isSolved: item.is_solved ?? false
1394
+ });
1395
+ }
1396
+ }
1397
+ if (!res.data?.has_more) break;
1398
+ pageToken = res.data.page_token;
1399
+ } catch (error) {
1400
+ logger.warn("\u83B7\u53D6\u6587\u6863\u8BC4\u8BBA\u5931\u8D25\uFF08\u53EF\u80FD\u65E0\u6743\u9650\uFF09", {
1401
+ fileToken,
1402
+ ...extractHttpErrorDetail(error)
1403
+ });
1404
+ break;
1405
+ }
1406
+ }
1407
+ return comments;
1408
+ }
1409
+ /**
1410
+ * 将纯文本按段落拆分为飞书文档 block 结构
1411
+ */
1412
+ textToBlocks(content) {
1413
+ return content.split("\n").map((line) => ({
1414
+ block_type: PARAGRAPH_BLOCK_TYPE,
1415
+ paragraph: {
1416
+ elements: [{ text_run: { content: line } }]
1417
+ }
1418
+ }));
1419
+ }
1420
+ async createDocument(title, content, folderToken) {
1421
+ try {
1422
+ const createRes = await this.client.docx.document.create({
1423
+ data: {
1424
+ title,
1425
+ ...folderToken ? { folder_token: folderToken } : {}
1426
+ }
1427
+ });
1428
+ if (createRes.code !== 0) {
1429
+ throw new Error(`\u521B\u5EFA\u6587\u6863\u5931\u8D25 (code=${createRes.code}): ${createRes.msg}`);
1430
+ }
1431
+ const documentId = createRes.data?.document?.document_id;
1432
+ if (!documentId) {
1433
+ throw new Error("\u521B\u5EFA\u6587\u6863\u6210\u529F\u4F46\u672A\u8FD4\u56DE document_id");
1434
+ }
1435
+ if (content.trim()) {
1436
+ const blocks = this.textToBlocks(content);
1437
+ const writeRes = await this.client.docx.documentBlockChildren.create({
1438
+ path: { document_id: documentId, block_id: documentId },
1439
+ data: { children: blocks, index: 0 }
1440
+ });
1441
+ if (writeRes.code !== 0) {
1442
+ logger.warn("\u6587\u6863\u5DF2\u521B\u5EFA\u4F46\u5199\u5165\u5185\u5BB9\u5931\u8D25", {
1443
+ documentId,
1444
+ code: writeRes.code,
1445
+ msg: writeRes.msg
1446
+ });
1447
+ }
1448
+ }
1449
+ const url = `https://feishu.cn/docx/${documentId}`;
1450
+ logger.info("\u6210\u529F\u521B\u5EFA\u98DE\u4E66\u6587\u6863", { documentId, title });
1451
+ return success({ documentId, title, url });
1452
+ } catch (error) {
1453
+ const detail = extractHttpErrorDetail(error);
1454
+ logger.error("FeishuOpen: \u521B\u5EFA\u6587\u6863\u5931\u8D25", { ...detail, title });
1455
+ const msg = error instanceof Error ? error.message : String(error);
1456
+ return failure(new Error(`FeishuOpen \u521B\u5EFA\u6587\u6863\u5931\u8D25: ${msg}`));
1457
+ }
1458
+ }
1459
+ async updateDocument(documentInput, content) {
1460
+ const parsed = parseFeishuDocumentId(documentInput);
1461
+ let { documentId } = parsed;
1462
+ try {
1463
+ if (parsed.isWiki) {
1464
+ const wiki = await this.resolveWikiToken(documentId);
1465
+ documentId = wiki.objToken;
1466
+ }
1467
+ const childrenRes = await this.client.docx.documentBlockChildren.get({
1468
+ path: { document_id: documentId, block_id: documentId },
1469
+ params: { page_size: 500 }
1470
+ });
1471
+ if (childrenRes.code !== 0) {
1472
+ throw new Error(`\u83B7\u53D6\u6587\u6863\u5B50\u5757\u5931\u8D25 (code=${childrenRes.code}): ${childrenRes.msg}`);
1473
+ }
1474
+ const items = childrenRes.data?.items ?? [];
1475
+ if (items.length > 0) {
1476
+ const deleteRes = await this.client.docx.documentBlockChildren.batchDelete({
1477
+ path: { document_id: documentId, block_id: documentId },
1478
+ data: { start_index: 0, end_index: items.length }
1479
+ });
1480
+ if (deleteRes.code !== 0) {
1481
+ throw new Error(`\u5220\u9664\u6587\u6863\u5B50\u5757\u5931\u8D25 (code=${deleteRes.code}): ${deleteRes.msg}`);
1482
+ }
1483
+ }
1484
+ if (content.trim()) {
1485
+ const blocks = this.textToBlocks(content);
1486
+ const writeRes = await this.client.docx.documentBlockChildren.create({
1487
+ path: { document_id: documentId, block_id: documentId },
1488
+ data: { children: blocks, index: 0 }
1489
+ });
1490
+ if (writeRes.code !== 0) {
1491
+ throw new Error(`\u5199\u5165\u6587\u6863\u5185\u5BB9\u5931\u8D25 (code=${writeRes.code}): ${writeRes.msg}`);
1492
+ }
1493
+ }
1494
+ const url = `https://feishu.cn/docx/${documentId}`;
1495
+ logger.info("\u6210\u529F\u66F4\u65B0\u98DE\u4E66\u6587\u6863", { documentId });
1496
+ return success({ documentId, url });
1497
+ } catch (error) {
1498
+ const detail = extractHttpErrorDetail(error);
1499
+ logger.error(`FeishuOpen: \u66F4\u65B0\u6587\u6863 ${documentId} \u5931\u8D25`, detail);
1500
+ const msg = error instanceof Error ? error.message : String(error);
1501
+ return failure(new Error(`FeishuOpen \u66F4\u65B0\u6587\u6863 ${documentId} \u5931\u8D25: ${msg}`));
1502
+ }
1503
+ }
1504
+ async getDocument(documentInput) {
1505
+ const parsed = parseFeishuDocumentId(documentInput);
1506
+ let { documentId } = parsed;
1507
+ let { fileType } = parsed;
1508
+ logger.debug("\u89E3\u6790\u98DE\u4E66\u6587\u6863\u6807\u8BC6", { input: documentInput, documentId, fileType });
1509
+ try {
1510
+ if (parsed.isWiki) {
1511
+ const wiki = await this.resolveWikiToken(documentId);
1512
+ logger.debug("Wiki token \u5DF2\u89E3\u6790", { wikiToken: documentId, objToken: wiki.objToken, objType: wiki.objType });
1513
+ documentId = wiki.objToken;
1514
+ fileType = wiki.objType;
1515
+ }
1516
+ const content = await this.getDocumentContent(documentId);
1517
+ const comments = await this.getDocumentComments(documentId, fileType);
1518
+ return success({
1519
+ documentId,
1520
+ fileType,
1521
+ content,
1522
+ comments: comments.length > 0 ? comments : void 0
1523
+ });
1524
+ } catch (error) {
1525
+ const detail = extractHttpErrorDetail(error);
1526
+ logger.error(`FeishuOpen: \u83B7\u53D6\u6587\u6863 ${documentId} \u5931\u8D25`, detail);
1527
+ const msg = error instanceof Error ? error.message : String(error);
1528
+ return failure(new Error(`FeishuOpen \u83B7\u53D6\u6587\u6863 ${documentId} \u5931\u8D25: ${msg}`));
1529
+ }
1530
+ }
1531
+ };
1532
+
1533
+ // src/services/platforms/JiraClient.ts
1534
+ var JiraClient = class extends BasePlatformClient {
1535
+ constructor(host, username, token) {
1536
+ super("Jira", {
1537
+ baseURL: host,
1538
+ auth: { type: "basic", username, password: token },
1539
+ timeout: 3e4
1540
+ });
1541
+ this.api.defaults.headers.common["Accept"] = "application/json";
1542
+ }
1543
+ async getIssue(issueKey) {
1544
+ return this.wrapApiCall(`\u83B7\u53D6\u4EFB\u52A1 ${issueKey}`, async () => {
1545
+ const url = `/rest/api/2/issue/${issueKey}?fields=summary,description,status,issuetype,comment`;
1546
+ const response = await this.api.get(url);
1547
+ return response.data;
1548
+ });
1549
+ }
1550
+ };
1551
+ function createJiraClient() {
1552
+ const { host, username, token } = config.jira;
1553
+ if (!host || !username || !token) return null;
1554
+ return new JiraClient(host, username, token);
1555
+ }
1556
+
1557
+ // src/services/mcpService.ts
1558
+ function toToolResult(result) {
1559
+ if (!result.success) {
1560
+ return {
1561
+ content: [{ type: "text", text: `Error: ${result.error.message}` }],
1562
+ isError: true
1563
+ };
1564
+ }
1565
+ const toolResult = {
1566
+ content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
1567
+ };
1568
+ if (typeof result.data === "object" && result.data !== null && !Array.isArray(result.data)) {
1569
+ toolResult.structuredContent = result.data;
1570
+ }
1571
+ return toolResult;
1572
+ }
1573
+ function truncateDiff(diffContent, maxSize, context) {
1574
+ const byteLength = Buffer.byteLength(diffContent, "utf-8");
1575
+ if (byteLength <= maxSize) {
1576
+ return diffContent;
1577
+ }
1578
+ const originalSizeMB = (byteLength / 1024 / 1024).toFixed(2);
1579
+ const maxSizeMB = (maxSize / 1024 / 1024).toFixed(2);
1580
+ logger.warn("Diff \u5185\u5BB9\u8FC7\u5927\uFF0C\u5DF2\u622A\u65AD", {
1581
+ project: context.project,
1582
+ repository: context.repository,
1583
+ prId: context.prId,
1584
+ originalSize: byteLength,
1585
+ maxSize,
1586
+ originalSizeMB,
1587
+ maxSizeMB
1588
+ });
1589
+ const buf = Buffer.from(diffContent, "utf-8");
1590
+ let cutoff = Math.min(maxSize, buf.length);
1591
+ const truncatedBuf = buf.subarray(0, cutoff);
1592
+ const lastNewline = truncatedBuf.lastIndexOf(10);
1593
+ if (lastNewline !== -1) {
1594
+ cutoff = lastNewline;
1595
+ } else {
1596
+ while (cutoff > 0 && (buf[cutoff] & 192) === 128) {
1597
+ cutoff--;
1598
+ }
1599
+ }
1600
+ const truncatedContent = buf.subarray(0, cutoff).toString("utf-8");
1601
+ const warningMessage = `\u26A0\uFE0F Diff \u5185\u5BB9\u8FC7\u5927 (${originalSizeMB}MB)\uFF0C\u5DF2\u622A\u65AD\u81F3 ${maxSizeMB}MB
1602
+ \u5B8C\u6574 diff \u5171 ${byteLength.toLocaleString()} \u5B57\u8282\uFF0C\u5F53\u524D\u663E\u793A\u524D ${cutoff.toLocaleString()} \u5B57\u8282
1603
+ \u5EFA\u8BAE\uFF1A\u4F7F\u7528\u66F4\u5C0F\u7684 contextLines \u53C2\u6570\u6216\u76F4\u63A5\u5728 Bitbucket \u4E2D\u67E5\u770B
1604
+
1605
+ ${"=".repeat(80)}
1606
+
1607
+ `;
1608
+ return `${warningMessage + truncatedContent}
1609
+
1610
+ ${"=".repeat(80)}
1611
+
1612
+ \u26A0\uFE0F \u5185\u5BB9\u5DF2\u622A\u65AD\uFF0C\u5269\u4F59 ${(byteLength - cutoff).toLocaleString()} \u5B57\u8282\u672A\u663E\u793A
1613
+ `;
1614
+ }
1615
+ var McpService = class _McpService {
1616
+ bitbucketClient;
1617
+ jiraClient;
1618
+ confluenceClient;
1619
+ feishuClient;
1620
+ feishuOpenClient;
1621
+ constructor(bitbucketClient = null, jiraClient = null, confluenceClient = null, feishuClient = null, feishuOpenClient = null) {
1622
+ this.bitbucketClient = bitbucketClient;
1623
+ this.jiraClient = jiraClient;
1624
+ this.confluenceClient = confluenceClient;
1625
+ this.feishuClient = feishuClient;
1626
+ this.feishuOpenClient = feishuOpenClient;
1627
+ }
1628
+ requireClient(client, name) {
1629
+ if (!client) throw new Error(`${name}\u5BA2\u6237\u7AEF\u672A\u914D\u7F6E`);
1630
+ return client;
1631
+ }
1632
+ requireBitbucket() {
1633
+ return this.requireClient(this.bitbucketClient, "Bitbucket");
1634
+ }
1635
+ requireJira() {
1636
+ return this.requireClient(this.jiraClient, "Jira");
1637
+ }
1638
+ requireConfluence() {
1639
+ return this.requireClient(this.confluenceClient, "Confluence");
1640
+ }
1641
+ requireFeishu() {
1642
+ return this.requireClient(this.feishuClient, "\u98DE\u4E66");
1643
+ }
1644
+ requireFeishuOpen() {
1645
+ return this.requireClient(this.feishuOpenClient, "\u98DE\u4E66\u5F00\u653E\u5E73\u53F0");
1646
+ }
1647
+ static create() {
1648
+ const bitbucket = createBitbucketClient();
1649
+ const jira = createJiraClient();
1650
+ const confluence = createConfluenceClient();
1651
+ const feishu = createFeishuClient();
1652
+ const feishuOpen = config.feishuOpen.appId && config.feishuOpen.appSecret ? new FeishuOpenClient({ appId: config.feishuOpen.appId, appSecret: config.feishuOpen.appSecret }) : null;
1653
+ const platforms = {
1654
+ bitbucket: !!bitbucket,
1655
+ jira: !!jira,
1656
+ confluence: !!confluence,
1657
+ feishu: !!feishu,
1658
+ feishuOpen: !!feishuOpen
1659
+ };
1660
+ logger.info("\u5E73\u53F0\u5BA2\u6237\u7AEF\u521D\u59CB\u5316\u5B8C\u6210", platforms);
1661
+ return new _McpService(bitbucket, jira, confluence, feishu, feishuOpen);
1662
+ }
1663
+ /**
1664
+ * 获取 PR diff(含截断逻辑)
1665
+ * 保留在 McpService 中因为有独立的业务逻辑
1666
+ */
1667
+ async getDiff(params, contextLines = 10, maxSize = config.mcp.maxDiffSize) {
1668
+ const client = this.requireBitbucket();
1669
+ const result = await client.getPullRequestDiff(params, contextLines);
1670
+ if (!result.success) {
1671
+ return toToolResult(result);
1672
+ }
1673
+ const diffContent = truncateDiff(result.data, maxSize, params);
1674
+ return {
1675
+ content: [{ type: "text", text: diffContent }]
1676
+ };
1677
+ }
1678
+ };
1679
+ var pullRequestSchema = z.object({
1680
+ id: z.number(),
1681
+ title: z.string(),
1682
+ description: z.string().optional(),
1683
+ state: z.string(),
1684
+ author: z.object({ displayName: z.string().optional() }).optional(),
1685
+ fromRef: z.object({ displayId: z.string().optional() }).optional(),
1686
+ toRef: z.object({ displayId: z.string().optional() }).optional()
1687
+ });
1688
+ var jiraIssueSchema = z.object({
1689
+ key: z.string(),
1690
+ fields: z.object({
1691
+ summary: z.string(),
1692
+ description: z.string().optional(),
1693
+ status: z.object({ name: z.string() }),
1694
+ issuetype: z.object({ name: z.string() }),
1695
+ comment: z.object({
1696
+ comments: z.array(
1697
+ z.object({
1698
+ body: z.string(),
1699
+ author: z.object({ displayName: z.string() }),
1700
+ created: z.string()
1701
+ })
1702
+ )
1703
+ }).optional()
1704
+ })
1705
+ });
1706
+ var confluencePageSchema = z.object({
1707
+ id: z.string(),
1708
+ type: z.string(),
1709
+ title: z.string(),
1710
+ space: z.object({ key: z.string().optional(), name: z.string().optional() }),
1711
+ version: z.object({
1712
+ number: z.number().optional(),
1713
+ when: z.string().optional(),
1714
+ by: z.string().optional()
1715
+ }),
1716
+ body: z.object({ storage: z.string().optional() }),
1717
+ history: z.object({
1718
+ createdBy: z.string().optional(),
1719
+ createdDate: z.string().optional()
1720
+ }),
1721
+ webUrl: z.string()
1722
+ });
1723
+ var confluenceChildPageSchema = z.object({
1724
+ id: z.string(),
1725
+ type: z.string(),
1726
+ title: z.string(),
1727
+ status: z.string(),
1728
+ space: z.object({ key: z.string().optional(), name: z.string().optional() }),
1729
+ version: z.object({
1730
+ number: z.number().optional(),
1731
+ when: z.string().optional(),
1732
+ by: z.string().optional()
1733
+ }),
1734
+ webUrl: z.string(),
1735
+ position: z.string().optional()
1736
+ });
1737
+ var confluenceChildPagesResultSchema = z.object({
1738
+ parentPageId: z.string(),
1739
+ totalChildren: z.number(),
1740
+ start: z.number(),
1741
+ limit: z.number(),
1742
+ children: z.array(confluenceChildPageSchema)
1743
+ });
1744
+ var confluenceCommentSchema = z.object({
1745
+ id: z.string(),
1746
+ body: z.string().optional(),
1747
+ author: z.string().optional(),
1748
+ createdAt: z.string().optional(),
1749
+ location: z.string().optional()
1750
+ });
1751
+ var confluenceCommentsResultSchema = z.object({
1752
+ pageId: z.string(),
1753
+ totalComments: z.number(),
1754
+ start: z.number(),
1755
+ limit: z.number(),
1756
+ comments: z.array(confluenceCommentSchema)
1757
+ });
1758
+ var feishuCommentSchema = z.object({
1759
+ id: z.string(),
1760
+ content: z.string(),
1761
+ workItemId: z.string(),
1762
+ workItemTypeKey: z.string(),
1763
+ createdAt: z.number(),
1764
+ operator: z.string()
1765
+ });
1766
+ var feishuWorkItemSchema = z.object({
1767
+ projectKey: z.string(),
1768
+ id: z.string(),
1769
+ name: z.string(),
1770
+ description: z.string().optional(),
1771
+ work_item_type_key: z.string(),
1772
+ work_item_type_name: z.string().optional(),
1773
+ comments: z.array(feishuCommentSchema).optional()
1774
+ });
1775
+ var feishuDocCommentSchema = z.object({
1776
+ commentId: z.string(),
1777
+ content: z.string(),
1778
+ userId: z.string(),
1779
+ createTime: z.number(),
1780
+ isSolved: z.boolean()
1781
+ });
1782
+ var feishuDocumentSchema = z.object({
1783
+ documentId: z.string(),
1784
+ fileType: z.string(),
1785
+ content: z.string(),
1786
+ comments: z.array(feishuDocCommentSchema).optional()
1787
+ });
1788
+ var feishuDocumentCreateResultSchema = z.object({
1789
+ documentId: z.string(),
1790
+ title: z.string(),
1791
+ url: z.string()
1792
+ });
1793
+ var feishuDocumentUpdateResultSchema = z.object({
1794
+ documentId: z.string(),
1795
+ url: z.string()
1796
+ });
1797
+
1798
+ // src/mcp/tools/types.ts
1799
+ function defineTool(name, config2, handler) {
1800
+ return { name, config: config2, handler };
1801
+ }
1802
+
1803
+ // src/mcp/tools/bitbucket-tools.ts
1804
+ function defineBitbucketTools() {
1805
+ return [
1806
+ defineTool(
1807
+ "get_pull_request",
1808
+ {
1809
+ description: "Get pull request details from Bitbucket",
1810
+ inputSchema: z.object({
1811
+ project: z.string().min(1).regex(/^[a-zA-Z0-9_~.-]+$/).describe("Bitbucket project key"),
1812
+ repository: z.string().min(1).regex(/^[a-zA-Z0-9_~.-]+$/).describe("Repository slug"),
1813
+ prId: z.number().int().positive().describe("Pull request ID")
1814
+ }),
1815
+ outputSchema: pullRequestSchema
1816
+ },
1817
+ async (args, service) => {
1818
+ const client = service.requireBitbucket();
1819
+ return toToolResult(await client.getPullRequest(args));
1820
+ }
1821
+ ),
1822
+ defineTool(
1823
+ "get_diff",
1824
+ {
1825
+ description: "Get pull request diff from Bitbucket",
1826
+ inputSchema: z.object({
1827
+ project: z.string().min(1).regex(/^[a-zA-Z0-9_~.-]+$/).describe("Bitbucket project key"),
1828
+ repository: z.string().min(1).regex(/^[a-zA-Z0-9_~.-]+$/).describe("Repository slug"),
1829
+ prId: z.number().int().positive().describe("Pull request ID"),
1830
+ contextLines: z.number().int().nonnegative().describe("Number of context lines").optional()
1831
+ })
1832
+ },
1833
+ async ({ project, repository, prId, contextLines }, service) => {
1834
+ return service.getDiff({ project, repository, prId }, contextLines);
1835
+ }
1836
+ )
1837
+ ];
1838
+ }
1839
+ function defineConfluenceTools() {
1840
+ return [
1841
+ defineTool(
1842
+ "get_confluence_page",
1843
+ {
1844
+ description: "Get Confluence page content by page ID",
1845
+ inputSchema: z.object({
1846
+ pageId: z.string().min(1).regex(/^\d+$/).describe("Confluence page ID")
1847
+ }),
1848
+ outputSchema: confluencePageSchema
1849
+ },
1850
+ async ({ pageId }, service) => {
1851
+ const client = service.requireConfluence();
1852
+ return toToolResult(await client.getPage(pageId));
1853
+ }
1854
+ ),
1855
+ defineTool(
1856
+ "get_confluence_child_pages",
1857
+ {
1858
+ description: "Get child pages of a Confluence page",
1859
+ inputSchema: z.object({
1860
+ pageId: z.string().min(1).regex(/^\d+$/).describe("Confluence page ID"),
1861
+ limit: z.number().int().positive().max(200).describe("Maximum number of child pages to return (default: 50)").optional(),
1862
+ expand: z.string().regex(/^[a-zA-Z.,]+$/).describe("Fields to expand (default: version,space)").optional()
1863
+ }),
1864
+ outputSchema: confluenceChildPagesResultSchema
1865
+ },
1866
+ async ({ pageId, limit, expand }, service) => {
1867
+ const client = service.requireConfluence();
1868
+ return toToolResult(await client.getChildPages(pageId, { limit, expand }));
1869
+ }
1870
+ ),
1871
+ defineTool(
1872
+ "get_confluence_page_comments",
1873
+ {
1874
+ description: "Get comments on a Confluence page",
1875
+ inputSchema: z.object({
1876
+ pageId: z.string().min(1).regex(/^\d+$/).describe("Confluence page ID"),
1877
+ limit: z.number().int().positive().max(200).describe("Maximum number of comments to return (default: 50)").optional()
1878
+ }),
1879
+ outputSchema: confluenceCommentsResultSchema
1880
+ },
1881
+ async ({ pageId, limit }, service) => {
1882
+ const client = service.requireConfluence();
1883
+ return toToolResult(await client.getPageComments(pageId, { limit }));
1884
+ }
1885
+ )
1886
+ ];
1887
+ }
1888
+ function defineFeishuOpenTools() {
1889
+ return [
1890
+ defineTool(
1891
+ "get_feishu_document",
1892
+ {
1893
+ description: "Get Feishu cloud document content and comments. Supports URL or document token.",
1894
+ inputSchema: z.object({
1895
+ documentId: z.string().describe(
1896
+ "Document identifier. Formats: (1) URL: https://xxx.feishu.cn/docx/ABC123DEF456; (2) Token: ABC123DEF456"
1897
+ )
1898
+ }),
1899
+ outputSchema: feishuDocumentSchema
1900
+ },
1901
+ async ({ documentId }, service) => {
1902
+ const client = service.requireFeishuOpen();
1903
+ return toToolResult(await client.getDocument(documentId));
1904
+ }
1905
+ ),
1906
+ defineTool(
1907
+ "create_feishu_document",
1908
+ {
1909
+ description: "Create a new Feishu cloud document with title and content.",
1910
+ inputSchema: z.object({
1911
+ title: z.string().min(1).describe("Document title"),
1912
+ content: z.string().describe("Document body content (plain text, one paragraph per line)"),
1913
+ folderToken: z.string().describe("Folder token to create the document in (optional)").optional()
1914
+ }),
1915
+ outputSchema: feishuDocumentCreateResultSchema
1916
+ },
1917
+ async ({ title, content, folderToken }, service) => {
1918
+ const client = service.requireFeishuOpen();
1919
+ return toToolResult(await client.createDocument(title, content, folderToken));
1920
+ }
1921
+ ),
1922
+ defineTool(
1923
+ "update_feishu_document",
1924
+ {
1925
+ description: "Update an existing Feishu cloud document content. Replaces all existing content with new content.",
1926
+ inputSchema: z.object({
1927
+ documentId: z.string().describe(
1928
+ "Document identifier. Formats: (1) URL: https://xxx.feishu.cn/docx/ABC123DEF456 or https://xxx.feishu.cn/wiki/ABC123; (2) Token: ABC123DEF456"
1929
+ ),
1930
+ content: z.string().describe("New document body content (plain text, one paragraph per line). Replaces all existing content.")
1931
+ }),
1932
+ outputSchema: feishuDocumentUpdateResultSchema
1933
+ },
1934
+ async ({ documentId, content }, service) => {
1935
+ const client = service.requireFeishuOpen();
1936
+ return toToolResult(await client.updateDocument(documentId, content));
1937
+ }
1938
+ )
1939
+ ];
1940
+ }
1941
+ function defineFeishuTools() {
1942
+ return [
1943
+ defineTool(
1944
+ "get_feishu_work_item",
1945
+ {
1946
+ description: "Get Feishu project work item details. Supports URL, prefixed ID, or pure number.",
1947
+ inputSchema: z.object({
1948
+ workItemId: z.string().describe(
1949
+ "Work item identifier. Formats: (1) URL: https://project.feishu.cn/example/issue/detail/12345678; (2) Prefixed: f-12345678, m-1234567890; (3) Number: 12345678"
1950
+ )
1951
+ }),
1952
+ outputSchema: feishuWorkItemSchema
1953
+ },
1954
+ async ({ workItemId }, service) => {
1955
+ const client = service.requireFeishu();
1956
+ return toToolResult(await client.getWorkItem(workItemId));
1957
+ }
1958
+ )
1959
+ ];
1960
+ }
1961
+ function defineJiraTools() {
1962
+ return [
1963
+ defineTool(
1964
+ "get_jira_issue",
1965
+ {
1966
+ description: "Get Jira issue details",
1967
+ inputSchema: z.object({
1968
+ issueKey: z.string().min(1).regex(/^[A-Z][A-Z0-9_]+-\d+$/).describe("Jira issue key (e.g., PROJ-123)")
1969
+ }),
1970
+ outputSchema: jiraIssueSchema
1971
+ },
1972
+ async ({ issueKey }, service) => {
1973
+ const client = service.requireJira();
1974
+ return toToolResult(await client.getIssue(issueKey));
1975
+ }
1976
+ )
1977
+ ];
1978
+ }
1979
+
1980
+ // src/mcp/tools/index.ts
1981
+ var cachedDefinitions = null;
1982
+ function collectToolDefinitions() {
1983
+ if (cachedDefinitions) return cachedDefinitions;
1984
+ const definitions = [];
1985
+ if (platformAvailability.bitbucket) {
1986
+ const tools = defineBitbucketTools();
1987
+ definitions.push(...tools);
1988
+ logger.debug("\u5DF2\u6CE8\u518C Bitbucket \u5DE5\u5177", { tools: tools.map((t) => t.name) });
1989
+ }
1990
+ if (platformAvailability.jira) {
1991
+ const tools = defineJiraTools();
1992
+ definitions.push(...tools);
1993
+ logger.debug("\u5DF2\u6CE8\u518C Jira \u5DE5\u5177", { tools: tools.map((t) => t.name) });
1994
+ }
1995
+ if (platformAvailability.confluence) {
1996
+ const tools = defineConfluenceTools();
1997
+ definitions.push(...tools);
1998
+ logger.debug("\u5DF2\u6CE8\u518C Confluence \u5DE5\u5177", { tools: tools.map((t) => t.name) });
1999
+ }
2000
+ if (platformAvailability.feishu) {
2001
+ const tools = defineFeishuTools();
2002
+ definitions.push(...tools);
2003
+ logger.debug("\u5DF2\u6CE8\u518C Feishu \u5DE5\u5177", { tools: tools.map((t) => t.name) });
2004
+ }
2005
+ if (platformAvailability.feishuOpen) {
2006
+ const tools = defineFeishuOpenTools();
2007
+ definitions.push(...tools);
2008
+ logger.debug("\u5DF2\u6CE8\u518C FeishuOpen \u5DE5\u5177", { tools: tools.map((t) => t.name) });
2009
+ }
2010
+ cachedDefinitions = definitions;
2011
+ return definitions;
2012
+ }
2013
+
2014
+ // src/mcp/server-factory.ts
2015
+ async function safeToolCall(toolName, handler) {
2016
+ const startTime = Date.now();
2017
+ logger.debug("MCP \u5DE5\u5177\u8C03\u7528\u5F00\u59CB", { toolName });
2018
+ try {
2019
+ const result = await handler();
2020
+ const durationMs = Date.now() - startTime;
2021
+ logger.debug("MCP \u5DE5\u5177\u8C03\u7528\u5B8C\u6210", {
2022
+ toolName,
2023
+ durationMs,
2024
+ isError: result.isError ?? false
2025
+ });
2026
+ return result;
2027
+ } catch (error) {
2028
+ const durationMs = Date.now() - startTime;
2029
+ const errorMessage = error instanceof Error ? error.message : String(error);
2030
+ logger.error("MCP \u5DE5\u5177\u8C03\u7528\u5F02\u5E38", { toolName, durationMs, error: errorMessage });
2031
+ const safeMessage = errorMessage.includes("://") ? `${toolName} \u8C03\u7528\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u53C2\u6570\u6216\u7A0D\u540E\u91CD\u8BD5` : errorMessage;
2032
+ return {
2033
+ content: [{ type: "text", text: `Error: ${safeMessage}` }],
2034
+ isError: true
2035
+ };
2036
+ }
2037
+ }
2038
+ function createMcpServer(mcpService) {
2039
+ const service = mcpService ?? McpService.create();
2040
+ const server = new McpServer(
2041
+ {
2042
+ name: "@fineorg/mcp-server",
2043
+ version: config.version
2044
+ },
2045
+ {
2046
+ capabilities: {
2047
+ tools: {}
2048
+ }
2049
+ }
2050
+ );
2051
+ for (const tool of collectToolDefinitions()) {
2052
+ server.registerTool(
2053
+ tool.name,
2054
+ tool.config,
2055
+ (args) => safeToolCall(tool.name, () => tool.handler(args, service))
2056
+ );
2057
+ }
2058
+ return server;
2059
+ }
2060
+
2061
+ // src/mcp/session-manager.ts
2062
+ var sharedService;
2063
+ function getSharedService() {
2064
+ if (!sharedService) sharedService = McpService.create();
2065
+ return sharedService;
2066
+ }
2067
+ function isSessionLimitReached(currentSize) {
2068
+ return currentSize >= config.mcp.maxSessions;
2069
+ }
2070
+ function jsonRpcError(code, message) {
2071
+ return JSON.stringify({
2072
+ jsonrpc: "2.0",
2073
+ error: { code, message },
2074
+ id: null
2075
+ });
2076
+ }
2077
+
2078
+ // src/mcp/startup.ts
2079
+ var SESSION_CLEANUP_INTERVAL_MS = 5 * 60 * 1e3;
2080
+ function bootstrapServer() {
2081
+ try {
2082
+ initLogger();
2083
+ validateConfig();
2084
+ logger.info("\u914D\u7F6E\u9A8C\u8BC1\u901A\u8FC7");
2085
+ validateMcpStartup();
2086
+ } catch (error) {
2087
+ logger.error("\u542F\u52A8\u9A8C\u8BC1\u5931\u8D25", {
2088
+ error: error instanceof Error ? error.message : String(error)
2089
+ });
2090
+ process.exit(1);
2091
+ }
2092
+ }
2093
+ function logStartupInfo(transport) {
2094
+ const authStatus = getMcpAuthStatus();
2095
+ const baseInfo = {
2096
+ authEnabled: authStatus.authEnabled,
2097
+ hasAllowedClients: authStatus.hasAllowedClients
2098
+ };
2099
+ {
2100
+ const transportConfig = config.mcp.http;
2101
+ baseInfo.host = transportConfig.host;
2102
+ baseInfo.port = transportConfig.port;
2103
+ }
2104
+ logger.info(`MCP Server (${transport}) \u542F\u52A8`, baseInfo);
2105
+ }
2106
+ function handleHealthCheck(res, transport) {
2107
+ const health = {
2108
+ status: "ok",
2109
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2110
+ transport
2111
+ };
2112
+ res.writeHead(200, { "Content-Type": "application/json" });
2113
+ res.end(JSON.stringify(health));
2114
+ }
2115
+ function registerGracefulShutdown(httpServer, cleanup, transport) {
2116
+ const shutdown = async () => {
2117
+ logger.info(`\u6B63\u5728\u5173\u95ED MCP Server (${transport})`);
2118
+ await cleanup();
2119
+ httpServer.close((error) => {
2120
+ if (error) {
2121
+ logger.error("\u5173\u95ED HTTP \u670D\u52A1\u5668\u5931\u8D25", {
2122
+ error: error instanceof Error ? error.message : String(error)
2123
+ });
2124
+ process.exit(1);
2125
+ }
2126
+ logger.info(`MCP Server (${transport}) \u5DF2\u5173\u95ED`);
2127
+ process.exit(0);
2128
+ });
2129
+ setTimeout(() => {
2130
+ logger.warn("\u5F3A\u5236\u9000\u51FA MCP Server");
2131
+ process.exit(1);
2132
+ }, 1e4);
2133
+ };
2134
+ process.on("SIGINT", shutdown);
2135
+ process.on("SIGTERM", shutdown);
2136
+ }
2137
+ function startSessionCleanup(sessions2, transport) {
2138
+ return setInterval(async () => {
2139
+ const now = Date.now();
2140
+ let cleaned = 0;
2141
+ for (const [sessionId, session] of sessions2.entries()) {
2142
+ if (now - session.lastActivity > config.mcp.sessionTimeoutMs) {
2143
+ try {
2144
+ await session.close();
2145
+ } catch (error) {
2146
+ logger.error("\u6E05\u7406\u8FC7\u671F\u4F1A\u8BDD\u5931\u8D25", {
2147
+ sessionId,
2148
+ error: error instanceof Error ? error.message : String(error)
2149
+ });
2150
+ }
2151
+ sessions2.delete(sessionId);
2152
+ cleaned++;
2153
+ }
2154
+ }
2155
+ if (cleaned > 0) {
2156
+ logger.info(`\u6E05\u7406\u8FC7\u671F ${transport} \u4F1A\u8BDD`, {
2157
+ cleaned,
2158
+ remaining: sessions2.size
2159
+ });
2160
+ }
2161
+ }, SESSION_CLEANUP_INTERVAL_MS);
2162
+ }
2163
+ function runMain(main) {
2164
+ bootstrapServer();
2165
+ main().catch((error) => {
2166
+ logger.error("MCP Server \u53D1\u751F\u672A\u6355\u83B7\u9519\u8BEF", { error });
2167
+ process.exit(1);
2168
+ });
2169
+ }
2170
+
2171
+ // src/index-http.ts
2172
+ var sessions = /* @__PURE__ */ new Map();
2173
+ function getClientIp(req) {
2174
+ if (config.mcp.trustProxy) {
2175
+ const forwarded = req.headers["x-forwarded-for"];
2176
+ const firstIp = (Array.isArray(forwarded) ? forwarded[0] : forwarded)?.split(",")[0]?.trim();
2177
+ if (firstIp) return firstIp;
2178
+ }
2179
+ return req.socket.remoteAddress || "unknown";
2180
+ }
2181
+ function validatePostRequest(req, res) {
2182
+ if (!validateHttpAuth(req)) {
2183
+ res.writeHead(401, { "Content-Type": "application/json" });
2184
+ res.end(jsonRpcError(-32e3, "Unauthorized"));
2185
+ return false;
2186
+ }
2187
+ const contentType = req.headers["content-type"];
2188
+ if (contentType && !contentType.includes("application/json")) {
2189
+ logger.warn("\u4E0D\u652F\u6301\u7684 Content-Type", {
2190
+ contentType,
2191
+ ip: getClientIp(req)
2192
+ });
2193
+ res.writeHead(415, { "Content-Type": "application/json" });
2194
+ res.end(jsonRpcError(-32600, "Unsupported Media Type: expected application/json"));
2195
+ return false;
2196
+ }
2197
+ return true;
2198
+ }
2199
+ async function parseRequestBody(req, res) {
2200
+ const { maxBodySize } = config.mcp;
2201
+ try {
2202
+ const buffers = [];
2203
+ let totalSize = 0;
2204
+ for await (const chunk of req) {
2205
+ totalSize += chunk.length;
2206
+ if (totalSize > maxBodySize) {
2207
+ logger.warn("\u8BF7\u6C42\u4F53\u8FC7\u5927", {
2208
+ readBytes: totalSize,
2209
+ maxBodySize,
2210
+ ip: getClientIp(req)
2211
+ });
2212
+ res.writeHead(413, { "Content-Type": "application/json" });
2213
+ res.end(jsonRpcError(-32600, "Request body too large"));
2214
+ return void 0;
2215
+ }
2216
+ buffers.push(chunk);
2217
+ }
2218
+ const bodyStr = Buffer.concat(buffers).toString();
2219
+ if (bodyStr) {
2220
+ return JSON.parse(bodyStr);
2221
+ }
2222
+ return void 0;
2223
+ } catch (error) {
2224
+ logger.error("\u89E3\u6790\u8BF7\u6C42\u4F53\u5931\u8D25", {
2225
+ error: error instanceof Error ? error.message : String(error)
2226
+ });
2227
+ res.writeHead(400, { "Content-Type": "application/json" });
2228
+ res.end(jsonRpcError(-32600, "Invalid JSON"));
2229
+ return void 0;
2230
+ }
2231
+ }
2232
+ async function handleExistingSession(req, res, sessionId, body) {
2233
+ if (sessionId && sessions.has(sessionId)) {
2234
+ const session = sessions.get(sessionId);
2235
+ session.lastActivity = Date.now();
2236
+ logger.debug("\u5904\u7406\u73B0\u6709\u4F1A\u8BDD\u8BF7\u6C42", { sessionId });
2237
+ try {
2238
+ await session.transport.handleRequest(req, res, body);
2239
+ } catch (error) {
2240
+ logger.error("\u5904\u7406\u4F1A\u8BDD\u8BF7\u6C42\u5931\u8D25", {
2241
+ sessionId,
2242
+ error: error instanceof Error ? error.message : String(error)
2243
+ });
2244
+ if (!res.headersSent) {
2245
+ res.writeHead(500, { "Content-Type": "application/json" });
2246
+ res.end(jsonRpcError(-32e3, "Internal server error"));
2247
+ }
2248
+ }
2249
+ return true;
2250
+ }
2251
+ if (sessionId && !sessions.has(sessionId)) {
2252
+ logger.info("\u4F1A\u8BDD\u4E0D\u5B58\u5728\u6216\u5DF2\u8FC7\u671F\uFF0C\u5BA2\u6237\u7AEF\u9700\u91CD\u65B0\u521D\u59CB\u5316", { sessionId });
2253
+ res.writeHead(404, { "Content-Type": "application/json" });
2254
+ res.end(jsonRpcError(-32e3, "Session not found. Please reinitialize."));
2255
+ return true;
2256
+ }
2257
+ return false;
2258
+ }
2259
+ async function handleNewSession(req, res, sessionId, body) {
2260
+ if (sessionId || !body || !isInitializeRequest(body)) {
2261
+ return false;
2262
+ }
2263
+ if (isSessionLimitReached(sessions.size)) {
2264
+ logger.warn("\u4F1A\u8BDD\u6570\u8FBE\u5230\u4E0A\u9650\uFF0C\u62D2\u7EDD\u65B0\u8FDE\u63A5", {
2265
+ currentSessions: sessions.size,
2266
+ maxSessions: config.mcp.maxSessions,
2267
+ ip: getClientIp(req)
2268
+ });
2269
+ res.writeHead(503, { "Content-Type": "application/json" });
2270
+ res.end(jsonRpcError(-32e3, "Too many sessions"));
2271
+ return true;
2272
+ }
2273
+ const transport = new StreamableHTTPServerTransport({
2274
+ sessionIdGenerator: () => randomUUID(),
2275
+ onsessioninitialized: (id) => {
2276
+ sessions.set(id, {
2277
+ transport,
2278
+ server,
2279
+ lastActivity: Date.now(),
2280
+ close: () => transport.close()
2281
+ });
2282
+ logger.info("HTTP \u4F1A\u8BDD\u5DF2\u521D\u59CB\u5316", {
2283
+ sessionId: id,
2284
+ ip: getClientIp(req),
2285
+ totalSessions: sessions.size
2286
+ });
2287
+ },
2288
+ onsessionclosed: (id) => {
2289
+ sessions.delete(id);
2290
+ logger.info("HTTP \u4F1A\u8BDD\u5DF2\u5173\u95ED", {
2291
+ sessionId: id,
2292
+ totalSessions: sessions.size
2293
+ });
2294
+ }
2295
+ });
2296
+ const server = createMcpServer(getSharedService());
2297
+ transport.onclose = () => {
2298
+ if (transport.sessionId) {
2299
+ sessions.delete(transport.sessionId);
2300
+ }
2301
+ };
2302
+ try {
2303
+ await server.connect(transport);
2304
+ await transport.handleRequest(req, res, body);
2305
+ } catch (error) {
2306
+ logger.error("\u521D\u59CB\u5316\u4F1A\u8BDD\u5931\u8D25", {
2307
+ error: error instanceof Error ? error.message : String(error)
2308
+ });
2309
+ if (!res.headersSent) {
2310
+ res.writeHead(500, { "Content-Type": "application/json" });
2311
+ res.end(jsonRpcError(-32e3, "Internal server error"));
2312
+ }
2313
+ }
2314
+ return true;
2315
+ }
2316
+ async function handlePostRequest(req, res) {
2317
+ if (!validatePostRequest(req, res)) {
2318
+ return;
2319
+ }
2320
+ const sessionId = req.headers["mcp-session-id"];
2321
+ const body = await parseRequestBody(req, res);
2322
+ if (res.headersSent) {
2323
+ return;
2324
+ }
2325
+ if (await handleExistingSession(req, res, sessionId, body)) {
2326
+ return;
2327
+ }
2328
+ if (await handleNewSession(req, res, sessionId, body)) {
2329
+ return;
2330
+ }
2331
+ logger.warn("\u65E0\u6548\u7684 HTTP \u8BF7\u6C42", { sessionId, hasBody: !!body });
2332
+ res.writeHead(400, { "Content-Type": "application/json" });
2333
+ res.end(jsonRpcError(-32e3, "Invalid request"));
2334
+ }
2335
+ async function handleSessionRequest(req, res, method) {
2336
+ if (!validateHttpAuth(req)) {
2337
+ res.writeHead(401, { "Content-Type": "text/plain" });
2338
+ res.end("Unauthorized");
2339
+ return;
2340
+ }
2341
+ const sessionId = req.headers["mcp-session-id"];
2342
+ if (!sessionId || !sessions.has(sessionId)) {
2343
+ res.writeHead(404, { "Content-Type": "application/json" });
2344
+ res.end(jsonRpcError(-32e3, "Session not found. Please reinitialize."));
2345
+ return;
2346
+ }
2347
+ const session = sessions.get(sessionId);
2348
+ try {
2349
+ await session.transport.handleRequest(req, res);
2350
+ } catch (error) {
2351
+ logger.error(`\u5904\u7406 ${method} \u8BF7\u6C42\u5931\u8D25`, {
2352
+ sessionId,
2353
+ error: error instanceof Error ? error.message : String(error)
2354
+ });
2355
+ if (!res.headersSent) {
2356
+ res.writeHead(500, { "Content-Type": "text/plain" });
2357
+ res.end("Internal server error");
2358
+ }
2359
+ }
2360
+ }
2361
+ runMain(async () => {
2362
+ logStartupInfo("HTTP");
2363
+ const cleanupTimer2 = startSessionCleanup(sessions, "HTTP");
2364
+ const rateLimiter = new RateLimiter(
2365
+ config.mcp.rateLimitWindowMs,
2366
+ config.mcp.rateLimitMax
2367
+ );
2368
+ const httpServer = http.createServer(async (req, res) => {
2369
+ res.setHeader("X-Content-Type-Options", "nosniff");
2370
+ res.setHeader("X-Frame-Options", "DENY");
2371
+ const clientIp = getClientIp(req);
2372
+ if (!rateLimiter.isAllowed(clientIp)) {
2373
+ res.writeHead(429, { "Content-Type": "application/json" });
2374
+ res.end(jsonRpcError(-32e3, "Too many requests"));
2375
+ return;
2376
+ }
2377
+ const url = new URL(req.url, "http://localhost");
2378
+ if (url.pathname === "/mcp") {
2379
+ if (req.method === "POST") {
2380
+ await handlePostRequest(req, res);
2381
+ return;
2382
+ }
2383
+ if (req.method === "GET") {
2384
+ await handleSessionRequest(req, res, "GET");
2385
+ return;
2386
+ }
2387
+ if (req.method === "DELETE") {
2388
+ await handleSessionRequest(req, res, "DELETE");
2389
+ return;
2390
+ }
2391
+ }
2392
+ if (req.method === "GET" && url.pathname === "/health") {
2393
+ handleHealthCheck(res, "http");
2394
+ return;
2395
+ }
2396
+ res.writeHead(404, { "Content-Type": "application/json" });
2397
+ res.end(JSON.stringify({ error: "Not Found" }));
2398
+ });
2399
+ httpServer.requestTimeout = 3e4;
2400
+ httpServer.headersTimeout = 1e4;
2401
+ httpServer.keepAliveTimeout = 65e3;
2402
+ httpServer.listen(config.mcp.http.port, config.mcp.http.host, () => {
2403
+ logger.info("MCP Server (HTTP) \u5DF2\u542F\u52A8", {
2404
+ url: `http://${config.mcp.http.host}:${config.mcp.http.port}`,
2405
+ mcpEndpoint: "/mcp",
2406
+ healthEndpoint: "/health"
2407
+ });
2408
+ });
2409
+ registerGracefulShutdown(httpServer, async () => {
2410
+ clearInterval(cleanupTimer2);
2411
+ rateLimiter.destroy();
2412
+ for (const [sessionId, session] of sessions.entries()) {
2413
+ try {
2414
+ await session.transport.close();
2415
+ logger.debug("\u4F1A\u8BDD\u5DF2\u5173\u95ED", { sessionId });
2416
+ } catch (error) {
2417
+ logger.error("\u5173\u95ED\u4F1A\u8BDD\u5931\u8D25", {
2418
+ sessionId,
2419
+ error: error instanceof Error ? error.message : String(error)
2420
+ });
2421
+ }
2422
+ }
2423
+ sessions.clear();
2424
+ }, "HTTP");
2425
+ });