feishu-mcp 0.1.9-test.1 → 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 +3 -2
- package/dist/index.js +4 -9
- package/dist/manager/sseConnectionManager.js +3 -3
- package/dist/server.js +5 -11
- package/dist/services/baseService.js +4 -1
- package/dist/services/callbackService.js +11 -10
- package/dist/utils/logger.js +15 -1
- package/package.json +1 -1
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
|
-
|
|
10
|
+
Logger.error("Failed to start server:", error.message);
|
|
10
11
|
}
|
|
11
12
|
else {
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
+
Logger.info(`[SSE Connection] Client disconnected: ${sessionId}`);
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
82
|
* 获取指定sessionId的传输实例
|
|
83
83
|
*/
|
|
84
84
|
getTransport(sessionId) {
|
|
85
|
-
|
|
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
|
@@ -50,12 +50,8 @@ export class FeishuMcpServer {
|
|
|
50
50
|
}, async () => {
|
|
51
51
|
await server.connect(transport);
|
|
52
52
|
});
|
|
53
|
-
Logger
|
|
54
|
-
|
|
55
|
-
};
|
|
56
|
-
Logger.error = (...args) => {
|
|
57
|
-
server.server.sendLoggingMessage({ level: 'error', data: args });
|
|
58
|
-
};
|
|
53
|
+
// 注意:在 stdio 模式下,Logger 会自动禁用输出,避免污染 MCP 协议
|
|
54
|
+
// 如果需要日志,可以通过 MCP 协议的 logging 消息传递
|
|
59
55
|
Logger.info('Server connected and ready to process requests');
|
|
60
56
|
}
|
|
61
57
|
async startHttpServer(port) {
|
|
@@ -118,7 +114,7 @@ export class FeishuMcpServer {
|
|
|
118
114
|
await transport.handleRequest(req, res, req.body);
|
|
119
115
|
}
|
|
120
116
|
catch (error) {
|
|
121
|
-
|
|
117
|
+
Logger.error('Error handling MCP request:', error);
|
|
122
118
|
if (!res.headersSent) {
|
|
123
119
|
res.status(500).json({
|
|
124
120
|
jsonrpc: '2.0',
|
|
@@ -144,7 +140,7 @@ export class FeishuMcpServer {
|
|
|
144
140
|
await transport.handleRequest(req, res);
|
|
145
141
|
}
|
|
146
142
|
catch (error) {
|
|
147
|
-
|
|
143
|
+
Logger.error('Error handling GET request:', error);
|
|
148
144
|
if (!res.headersSent) {
|
|
149
145
|
res.status(500).send('Internal server error');
|
|
150
146
|
}
|
|
@@ -166,7 +162,7 @@ export class FeishuMcpServer {
|
|
|
166
162
|
}
|
|
167
163
|
}
|
|
168
164
|
catch (error) {
|
|
169
|
-
|
|
165
|
+
Logger.error('Error handling DELETE request:', error);
|
|
170
166
|
if (!res.headersSent) {
|
|
171
167
|
res.status(500).send('Internal server error');
|
|
172
168
|
}
|
|
@@ -244,11 +240,9 @@ export class FeishuMcpServer {
|
|
|
244
240
|
*/
|
|
245
241
|
async startCallbackServer(port) {
|
|
246
242
|
const app = express();
|
|
247
|
-
console.log(`[Callback Server] startCallbackServer`);
|
|
248
243
|
// 只注册callback接口
|
|
249
244
|
app.get('/callback', callback);
|
|
250
245
|
app.listen(port, '0.0.0.0', () => {
|
|
251
|
-
// 使用console.log确保在stdio模式下也能看到日志
|
|
252
246
|
Logger.info(`Callback server listening on port ${port}`);
|
|
253
247
|
Logger.info(`Callback endpoint available at http://localhost:${port}/callback`);
|
|
254
248
|
});
|
|
@@ -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
|
-
|
|
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
|
-
|
|
28
|
+
Logger.debug(`[callback] query:`, req.query);
|
|
28
29
|
if (!code) {
|
|
29
|
-
|
|
30
|
+
Logger.warn('[callback] 缺少code参数');
|
|
30
31
|
return sendFail(res, '缺少code参数', CODE.PARAM_ERROR);
|
|
31
32
|
}
|
|
32
33
|
if (!state) {
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
+
Logger.debug('[callback] feishu userInfo:', userInfo);
|
|
91
92
|
}
|
|
92
93
|
return sendSuccess(res, { ...data, userInfo, clientKey });
|
|
93
94
|
}
|
|
94
95
|
catch (e) {
|
|
95
|
-
|
|
96
|
+
Logger.error('[callback] 请求飞书token或用户信息失败:', e);
|
|
96
97
|
return sendFail(res, `请求飞书token或用户信息失败: ${e}`, CODE.CUSTOM);
|
|
97
98
|
}
|
|
98
99
|
}
|
package/dist/utils/logger.js
CHANGED
|
@@ -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
|
-
|
|
113
|
+
// 在 stdio 模式下不输出错误,避免污染 MCP 协议
|
|
114
|
+
if (!this.isStdioMode()) {
|
|
115
|
+
console.error('写入日志文件失败:', error);
|
|
116
|
+
}
|
|
103
117
|
}
|
|
104
118
|
}
|
|
105
119
|
/**
|