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