feishu-mcp 0.1.8 → 0.1.9-test.2

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,32 +13,29 @@ 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
+ // 在stdio模式下也需要启动HTTP服务器以提供callback接口
24
+ // 启动最小化的HTTP服务器(只提供callback接口)
25
+ await server.startCallbackServer(config.server.port);
23
26
  await server.connect(transport);
24
27
  }
25
28
  else {
26
- 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}...`);
27
30
  await server.startHttpServer(config.server.port);
28
31
  }
29
32
  }
30
33
  // 跨平台兼容的方式检查是否直接运行
31
34
  const currentFilePath = fileURLToPath(import.meta.url);
32
35
  const executedFilePath = resolve(process.argv[1]);
33
- console.log(`meta.url:${currentFilePath} argv:${executedFilePath}`);
34
36
  if (currentFilePath === executedFilePath) {
35
- console.log(`startServer`);
36
37
  startServer().catch((error) => {
37
- console.error('Failed to start server:', error);
38
+ Logger.error('Failed to start server:', error);
38
39
  process.exit(1);
39
40
  });
40
41
  }
41
- else {
42
- console.log(`not startServer`);
43
- }
@@ -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
@@ -8,6 +8,7 @@ import { SSEConnectionManager } from './manager/sseConnectionManager.js';
8
8
  import { FeishuMcp } from './mcp/feishuMcp.js';
9
9
  import { callback } from './services/callbackService.js';
10
10
  import { UserAuthManager, UserContextManager, getBaseUrl, TokenCacheManager, TokenRefreshManager } from './utils/auth/index.js';
11
+ import { Config } from './utils/config.js';
11
12
  export class FeishuMcpServer {
12
13
  constructor() {
13
14
  Object.defineProperty(this, "connectionManager", {
@@ -40,13 +41,17 @@ export class FeishuMcpServer {
40
41
  }
41
42
  async connect(transport) {
42
43
  const server = new FeishuMcp();
43
- await server.connect(transport);
44
- Logger.info = (...args) => {
45
- server.server.sendLoggingMessage({ level: 'info', data: args });
46
- };
47
- Logger.error = (...args) => {
48
- server.server.sendLoggingMessage({ level: 'error', data: args });
49
- };
44
+ // stdio模式只能本地运行,使用localhost + 配置的端口
45
+ const config = Config.getInstance();
46
+ const baseUrl = `http://localhost:${config.server.port}`;
47
+ await this.userContextManager.run({
48
+ userKey: 'stdio',
49
+ baseUrl: baseUrl
50
+ }, async () => {
51
+ await server.connect(transport);
52
+ });
53
+ // 注意:在 stdio 模式下,Logger 会自动禁用输出,避免污染 MCP 协议
54
+ // 如果需要日志,可以通过 MCP 协议的 logging 消息传递
50
55
  Logger.info('Server connected and ready to process requests');
51
56
  }
52
57
  async startHttpServer(port) {
@@ -109,7 +114,7 @@ export class FeishuMcpServer {
109
114
  await transport.handleRequest(req, res, req.body);
110
115
  }
111
116
  catch (error) {
112
- console.error('Error handling MCP request:', error);
117
+ Logger.error('Error handling MCP request:', error);
113
118
  if (!res.headersSent) {
114
119
  res.status(500).json({
115
120
  jsonrpc: '2.0',
@@ -135,7 +140,7 @@ export class FeishuMcpServer {
135
140
  await transport.handleRequest(req, res);
136
141
  }
137
142
  catch (error) {
138
- console.error('Error handling GET request:', error);
143
+ Logger.error('Error handling GET request:', error);
139
144
  if (!res.headersSent) {
140
145
  res.status(500).send('Internal server error');
141
146
  }
@@ -157,7 +162,7 @@ export class FeishuMcpServer {
157
162
  }
158
163
  }
159
164
  catch (error) {
160
- console.error('Error handling DELETE request:', error);
165
+ Logger.error('Error handling DELETE request:', error);
161
166
  if (!res.headersSent) {
162
167
  res.status(500).send('Internal server error');
163
168
  }
@@ -228,4 +233,18 @@ export class FeishuMcpServer {
228
233
  Logger.info(`StreamableHTTP endpoint available at http://localhost:${port}/mcp`);
229
234
  });
230
235
  }
236
+ /**
237
+ * 启动最小化的HTTP服务器(仅提供callback接口)
238
+ * 用于stdio模式下提供OAuth回调功能
239
+ * @param port 服务器端口
240
+ */
241
+ async startCallbackServer(port) {
242
+ const app = express();
243
+ // 只注册callback接口
244
+ app.get('/callback', callback);
245
+ app.listen(port, '0.0.0.0', () => {
246
+ Logger.info(`Callback server listening on port ${port}`);
247
+ Logger.info(`Callback endpoint available at http://localhost:${port}/callback`);
248
+ });
249
+ }
231
250
  }
@@ -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.8",
3
+ "version": "0.1.9-test.2",
4
4
  "description": "Model Context Protocol server for Feishu integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,7 +23,8 @@
23
23
  "format": "prettier --write \"src/**/*.ts\"",
24
24
  "inspect": "pnpx @modelcontextprotocol/inspector",
25
25
  "prepare": "pnpm run build",
26
- "pub:release": "pnpm build && npm publish"
26
+ "pub:release": "pnpm build && npm publish",
27
+ "pub:test": "pnpm build && npm publish --tag test"
27
28
  },
28
29
  "engines": {
29
30
  "node": "^20.17.0"