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 +3 -2
- package/dist/index.js +7 -9
- package/dist/manager/sseConnectionManager.js +3 -3
- package/dist/server.js +29 -10
- package/dist/services/baseService.js +4 -1
- package/dist/services/callbackService.js +11 -10
- package/dist/utils/logger.js +15 -1
- package/package.json +3 -2
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,32 +13,29 @@ 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
|
+
// 在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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feishu-mcp",
|
|
3
|
-
"version": "0.1.
|
|
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"
|