feishu-mcp 0.1.9-test.1 → 0.1.9-test.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2,14 +2,15 @@
2
2
  import { resolve } from "path";
3
3
  import { config } from "dotenv";
4
4
  import { startServer } from "./index.js";
5
+ import { Logger } from "./utils/logger.js";
5
6
  // Load .env from the current working directory
6
7
  config({ path: resolve(process.cwd(), ".env") });
7
8
  startServer().catch((error) => {
8
9
  if (error instanceof Error) {
9
- console.error("Failed to start server:", error.message);
10
+ Logger.error("Failed to start server:", error.message);
10
11
  }
11
12
  else {
12
- console.error("Failed to start server with unknown error:", error);
13
+ Logger.error("Failed to start server with unknown error:", error);
13
14
  }
14
15
  process.exit(1);
15
16
  });
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
2
  import { FeishuMcpServer } from "./server.js";
3
3
  import { Config } from "./utils/config.js";
4
+ import { Logger } from "./utils/logger.js";
4
5
  import { fileURLToPath } from 'url';
5
6
  import { resolve } from 'path';
6
7
  export async function startServer() {
@@ -12,12 +13,11 @@ export async function startServer() {
12
13
  config.printConfig(isStdioMode);
13
14
  // 验证配置
14
15
  if (!config.validate()) {
15
- console.error("配置验证失败,无法启动服务器");
16
+ Logger.error("配置验证失败,无法启动服务器");
16
17
  process.exit(1);
17
18
  }
18
19
  // 创建MCP服务器
19
20
  const server = new FeishuMcpServer();
20
- console.log(`isStdioMode:${isStdioMode}`);
21
21
  if (isStdioMode) {
22
22
  const transport = new StdioServerTransport();
23
23
  // 在stdio模式下也需要启动HTTP服务器以提供callback接口
@@ -26,21 +26,16 @@ export async function startServer() {
26
26
  await server.connect(transport);
27
27
  }
28
28
  else {
29
- console.log(`Initializing Feishu MCP Server in HTTP mode on port ${config.server.port}...`);
29
+ Logger.info(`Initializing Feishu MCP Server in HTTP mode on port ${config.server.port}...`);
30
30
  await server.startHttpServer(config.server.port);
31
31
  }
32
32
  }
33
33
  // 跨平台兼容的方式检查是否直接运行
34
34
  const currentFilePath = fileURLToPath(import.meta.url);
35
35
  const executedFilePath = resolve(process.argv[1]);
36
- console.log(`meta.url:${currentFilePath} argv:${executedFilePath}`);
37
36
  if (currentFilePath === executedFilePath) {
38
- console.log(`startServer`);
39
37
  startServer().catch((error) => {
40
- console.error('Failed to start server:', error);
38
+ Logger.error('Failed to start server:', error);
41
39
  process.exit(1);
42
40
  });
43
41
  }
44
- else {
45
- console.log(`not startServer`);
46
- }
@@ -55,7 +55,7 @@ export class SSEConnectionManager {
55
55
  addConnection(sessionId, transport, req, res) {
56
56
  this.transports[sessionId] = transport;
57
57
  this.connections.set(sessionId, { res });
58
- console.info(`[SSE Connection] Client connected: ${sessionId}`);
58
+ Logger.info(`[SSE Connection] Client connected: ${sessionId}`);
59
59
  req.on('close', () => {
60
60
  this.removeConnection(sessionId);
61
61
  });
@@ -76,13 +76,13 @@ export class SSEConnectionManager {
76
76
  }
77
77
  delete this.transports[sessionId];
78
78
  this.connections.delete(sessionId);
79
- console.info(`[SSE Connection] Client disconnected: ${sessionId}`);
79
+ Logger.info(`[SSE Connection] Client disconnected: ${sessionId}`);
80
80
  }
81
81
  /**
82
82
  * 获取指定sessionId的传输实例
83
83
  */
84
84
  getTransport(sessionId) {
85
- console.info(`[SSE Connection] Getting transport for sessionId: ${sessionId}`);
85
+ Logger.debug(`[SSE Connection] Getting transport for sessionId: ${sessionId}`);
86
86
  return this.transports[sessionId];
87
87
  }
88
88
  /**
package/dist/server.js CHANGED
@@ -29,6 +29,12 @@ export class FeishuMcpServer {
29
29
  writable: true,
30
30
  value: void 0
31
31
  });
32
+ Object.defineProperty(this, "callbackServer", {
33
+ enumerable: true,
34
+ configurable: true,
35
+ writable: true,
36
+ value: null
37
+ }); // stdio 模式下的 callback 服务器实例
32
38
  this.connectionManager = new SSEConnectionManager();
33
39
  this.userAuthManager = UserAuthManager.getInstance();
34
40
  this.userContextManager = UserContextManager.getInstance();
@@ -50,14 +56,40 @@ export class FeishuMcpServer {
50
56
  }, async () => {
51
57
  await server.connect(transport);
52
58
  });
53
- Logger.info = (...args) => {
54
- server.server.sendLoggingMessage({ level: 'info', data: args });
55
- };
56
- Logger.error = (...args) => {
57
- server.server.sendLoggingMessage({ level: 'error', data: args });
58
- };
59
+ // 监听 transport 关闭事件,清理 callback 服务器
60
+ // 对于 stdio 模式,监听 stdin 关闭事件
61
+ if (process.stdin && typeof process.stdin.on === 'function') {
62
+ process.stdin.on('close', () => {
63
+ this.stopCallbackServer();
64
+ });
65
+ process.stdin.on('end', () => {
66
+ this.stopCallbackServer();
67
+ });
68
+ }
69
+ // 监听进程退出事件,确保清理资源
70
+ process.on('SIGINT', () => {
71
+ this.stopCallbackServer();
72
+ process.exit(0);
73
+ });
74
+ process.on('SIGTERM', () => {
75
+ this.stopCallbackServer();
76
+ process.exit(0);
77
+ });
78
+ // 注意:在 stdio 模式下,Logger 会自动禁用输出,避免污染 MCP 协议
79
+ // 如果需要日志,可以通过 MCP 协议的 logging 消息传递
59
80
  Logger.info('Server connected and ready to process requests');
60
81
  }
82
+ /**
83
+ * 停止 callback 服务器
84
+ */
85
+ stopCallbackServer() {
86
+ if (this.callbackServer) {
87
+ this.callbackServer.close(() => {
88
+ Logger.info('Callback server stopped');
89
+ });
90
+ this.callbackServer = null;
91
+ }
92
+ }
61
93
  async startHttpServer(port) {
62
94
  const app = express();
63
95
  const transports = {};
@@ -118,7 +150,7 @@ export class FeishuMcpServer {
118
150
  await transport.handleRequest(req, res, req.body);
119
151
  }
120
152
  catch (error) {
121
- console.error('Error handling MCP request:', error);
153
+ Logger.error('Error handling MCP request:', error);
122
154
  if (!res.headersSent) {
123
155
  res.status(500).json({
124
156
  jsonrpc: '2.0',
@@ -144,7 +176,7 @@ export class FeishuMcpServer {
144
176
  await transport.handleRequest(req, res);
145
177
  }
146
178
  catch (error) {
147
- console.error('Error handling GET request:', error);
179
+ Logger.error('Error handling GET request:', error);
148
180
  if (!res.headersSent) {
149
181
  res.status(500).send('Internal server error');
150
182
  }
@@ -166,7 +198,7 @@ export class FeishuMcpServer {
166
198
  }
167
199
  }
168
200
  catch (error) {
169
- console.error('Error handling DELETE request:', error);
201
+ Logger.error('Error handling DELETE request:', error);
170
202
  if (!res.headersSent) {
171
203
  res.status(500).send('Internal server error');
172
204
  }
@@ -244,13 +276,26 @@ export class FeishuMcpServer {
244
276
  */
245
277
  async startCallbackServer(port) {
246
278
  const app = express();
247
- console.log(`[Callback Server] startCallbackServer`);
248
279
  // 只注册callback接口
249
280
  app.get('/callback', callback);
250
- app.listen(port, '0.0.0.0', () => {
251
- // 使用console.log确保在stdio模式下也能看到日志
252
- Logger.info(`Callback server listening on port ${port}`);
253
- Logger.info(`Callback endpoint available at http://localhost:${port}/callback`);
281
+ return new Promise((resolve, reject) => {
282
+ const server = app.listen(port, '0.0.0.0', () => {
283
+ this.callbackServer = server;
284
+ Logger.info(`Callback server listening on port ${port}`);
285
+ Logger.info(`Callback endpoint available at http://localhost:${port}/callback`);
286
+ resolve();
287
+ });
288
+ server.on('error', (err) => {
289
+ if (err.code === 'EADDRINUSE') {
290
+ // 端口被占用,说明其他进程已经启动了 callback 服务器
291
+ // 这是正常的,静默处理即可(多个 stdio 进程共享同一个 callback 服务器)
292
+ Logger.debug(`Port ${port} is already in use, callback server may already be running`);
293
+ resolve(); // 不抛出错误,因为 callback 服务器已经存在
294
+ }
295
+ else {
296
+ reject(err);
297
+ }
298
+ });
254
299
  });
255
300
  }
256
301
  }
@@ -309,8 +309,11 @@ export class BaseApiService {
309
309
  */
310
310
  generateUserAuthUrl(baseUrl, userKey) {
311
311
  const { appId, appSecret } = Config.getInstance().feishu;
312
+ const config = Config.getInstance();
312
313
  const clientKey = AuthUtils.generateClientKey(userKey);
313
- const redirect_uri = `${baseUrl}/callback`;
314
+ // 如果 baseUrl 为空,使用默认值(stdio 模式)
315
+ const finalBaseUrl = baseUrl || `http://localhost:${config.server.port}`;
316
+ const redirect_uri = `${finalBaseUrl}/callback`;
314
317
  const scope = encodeURIComponent('base:app:read bitable:app bitable:app:readonly board:whiteboard:node:read board:whiteboard:node:create contact:user.employee_id:readonly docs:document.content:read docx:document docx:document.block:convert docx:document:create docx:document:readonly drive:drive drive:drive:readonly drive:file drive:file:upload sheets:spreadsheet sheets:spreadsheet:readonly space:document:retrieve space:folder:create wiki:space:read wiki:space:retrieve wiki:wiki wiki:wiki:readonly offline_access');
315
318
  const state = AuthUtils.encodeState(appId, appSecret, clientKey, redirect_uri);
316
319
  return `https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id=${appId}&redirect_uri=${encodeURIComponent(redirect_uri)}&scope=${scope}&state=${state}`;
@@ -2,6 +2,7 @@ import { AuthService } from './feishuAuthService.js';
2
2
  import { Config } from '../utils/config.js';
3
3
  import { renderFeishuAuthResultHtml } from '../utils/document.js';
4
4
  import { AuthUtils, TokenCacheManager } from '../utils/auth/index.js';
5
+ import { Logger } from '../utils/logger.js';
5
6
  // 通用响应码
6
7
  const CODE = {
7
8
  SUCCESS: 0,
@@ -24,28 +25,28 @@ const config = Config.getInstance();
24
25
  export async function callback(req, res) {
25
26
  const code = req.query.code;
26
27
  const state = req.query.state;
27
- console.log(`[callback] query:`, req.query);
28
+ Logger.debug(`[callback] query:`, req.query);
28
29
  if (!code) {
29
- console.log('[callback] 缺少code参数');
30
+ Logger.warn('[callback] 缺少code参数');
30
31
  return sendFail(res, '缺少code参数', CODE.PARAM_ERROR);
31
32
  }
32
33
  if (!state) {
33
- console.log('[callback] 缺少state参数');
34
+ Logger.warn('[callback] 缺少state参数');
34
35
  return sendFail(res, '缺少state参数', CODE.PARAM_ERROR);
35
36
  }
36
37
  // 解析state参数
37
38
  const stateData = AuthUtils.decodeState(state);
38
39
  if (!stateData) {
39
- console.log('[callback] state参数解析失败');
40
+ Logger.warn('[callback] state参数解析失败');
40
41
  return sendFail(res, 'state参数格式错误', CODE.PARAM_ERROR);
41
42
  }
42
43
  const { appId, appSecret, clientKey, redirectUri } = stateData;
43
- console.log(`[callback] 解析state成功:`, { appId, clientKey, redirectUri });
44
+ Logger.debug(`[callback] 解析state成功:`, { appId, clientKey, redirectUri });
44
45
  // 验证state中的appId和appSecret是否与配置匹配
45
46
  const configAppId = config.feishu.appId;
46
47
  const configAppSecret = config.feishu.appSecret;
47
48
  if (appId !== configAppId || appSecret !== configAppSecret) {
48
- console.log('[callback] state中的appId或appSecret与配置不匹配');
49
+ Logger.warn('[callback] state中的appId或appSecret与配置不匹配');
49
50
  return sendFail(res, 'state参数验证失败', CODE.PARAM_ERROR);
50
51
  }
51
52
  // 使用从state中解析的redirect_uri,如果没有则使用默认值
@@ -62,7 +63,7 @@ export async function callback(req, res) {
62
63
  code_verifier
63
64
  });
64
65
  const data = (tokenResp && typeof tokenResp === 'object') ? tokenResp : undefined;
65
- console.log('[callback] feishu response:', data);
66
+ Logger.debug('[callback] feishu response:', data);
66
67
  if (!data || data.code !== 0 || !data.access_token) {
67
68
  return sendFail(res, `获取 access_token 失败,飞书返回: ${JSON.stringify(tokenResp)}`, CODE.CUSTOM);
68
69
  }
@@ -80,19 +81,19 @@ export async function callback(req, res) {
80
81
  // 缓存token信息
81
82
  const refreshTtl = data.refresh_token_expires_in || 3600 * 24 * 365; // 默认1年
82
83
  tokenCacheManager.cacheUserToken(clientKey, data, refreshTtl);
83
- console.log(`[callback] token已缓存到clientKey: ${clientKey}`);
84
+ Logger.info(`[callback] token已缓存到clientKey: ${clientKey}`);
84
85
  }
85
86
  // 获取用户信息
86
87
  const access_token = data.access_token;
87
88
  let userInfo = null;
88
89
  if (access_token) {
89
90
  userInfo = await authService.getUserInfo(access_token);
90
- console.log('[callback] feishu userInfo:', userInfo);
91
+ Logger.debug('[callback] feishu userInfo:', userInfo);
91
92
  }
92
93
  return sendSuccess(res, { ...data, userInfo, clientKey });
93
94
  }
94
95
  catch (e) {
95
- console.error('[callback] 请求飞书token或用户信息失败:', e);
96
+ Logger.error('[callback] 请求飞书token或用户信息失败:', e);
96
97
  return sendFail(res, `请求飞书token或用户信息失败: ${e}`, CODE.CUSTOM);
97
98
  }
98
99
  }
@@ -18,6 +18,13 @@ import * as path from 'path';
18
18
  * 提供可配置的日志记录功能,支持不同日志级别和格式化
19
19
  */
20
20
  export class Logger {
21
+ /**
22
+ * 检查是否处于 stdio 模式
23
+ * @returns 是否处于 stdio 模式
24
+ */
25
+ static isStdioMode() {
26
+ return process.env.NODE_ENV === "cli" || process.argv.includes("--stdio");
27
+ }
21
28
  /**
22
29
  * 配置日志管理器
23
30
  * @param config 日志配置项
@@ -45,6 +52,10 @@ export class Logger {
45
52
  * @returns 是否可输出
46
53
  */
47
54
  static canLog(level) {
55
+ // 在 stdio 模式下,禁用所有日志输出(避免污染 MCP 协议)
56
+ if (this.isStdioMode()) {
57
+ return false;
58
+ }
48
59
  return this.config.enabled && level >= this.config.minLevel;
49
60
  }
50
61
  /**
@@ -99,7 +110,10 @@ export class Logger {
99
110
  fs.appendFileSync(this.config.logFilePath, logString);
100
111
  }
101
112
  catch (error) {
102
- console.error('写入日志文件失败:', error);
113
+ // 在 stdio 模式下不输出错误,避免污染 MCP 协议
114
+ if (!this.isStdioMode()) {
115
+ console.error('写入日志文件失败:', error);
116
+ }
103
117
  }
104
118
  }
105
119
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "feishu-mcp",
3
- "version": "0.1.9-test.1",
3
+ "version": "0.1.9-test.3",
4
4
  "description": "Model Context Protocol server for Feishu integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",