lark-mcp-server 0.0.1

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.
Files changed (3) hide show
  1. package/README.md +7 -0
  2. package/index.js +303 -0
  3. package/package.json +33 -0
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # lark-mcp-server
2
+
3
+ 飞书 MCP Server - 内部使用。
4
+
5
+ ## License
6
+
7
+ MIT
package/index.js ADDED
@@ -0,0 +1,303 @@
1
+ #!/usr/bin/env node
2
+
3
+ import open from 'open';
4
+ import EventSource from 'eventsource';
5
+ import fs from 'fs';
6
+ import os from 'os';
7
+ import path from 'path';
8
+
9
+ // 解析命令行参数
10
+ const args = process.argv.slice(2);
11
+ const params = {};
12
+ for (const arg of args) {
13
+ const match = arg.match(/^--(\w+)=(.+)$/);
14
+ if (match) {
15
+ params[match[1]] = match[2];
16
+ }
17
+ }
18
+
19
+ const SERVER_URL = params.server || 'https://basic.dev.peblla.top/api/feishu';
20
+ const USER_NAME = params.user || 'default';
21
+
22
+ // 日志输出到 stderr,不影响 stdio 通信
23
+ const log = (...args) => console.error('[lark-mcp-proxy]', ...args);
24
+
25
+ // Token 本地存储
26
+ const TOKEN_DIR = path.join(os.homedir(), '.lark-mcp');
27
+ const TOKEN_FILE = path.join(TOKEN_DIR, 'tokens.json');
28
+
29
+ // 加载本地 token
30
+ function loadLocalToken(userName) {
31
+ try {
32
+ if (fs.existsSync(TOKEN_FILE)) {
33
+ const data = JSON.parse(fs.readFileSync(TOKEN_FILE, 'utf-8'));
34
+ return data[userName] || null;
35
+ }
36
+ } catch (e) {
37
+ log('Failed to load local token:', e.message);
38
+ }
39
+ return null;
40
+ }
41
+
42
+ // 保存 token 到本地
43
+ function saveLocalToken(userName, tokenInfo) {
44
+ try {
45
+ // 确保目录存在
46
+ if (!fs.existsSync(TOKEN_DIR)) {
47
+ fs.mkdirSync(TOKEN_DIR, { recursive: true, mode: 0o700 });
48
+ }
49
+
50
+ // 读取现有数据
51
+ let data = {};
52
+ if (fs.existsSync(TOKEN_FILE)) {
53
+ data = JSON.parse(fs.readFileSync(TOKEN_FILE, 'utf-8'));
54
+ }
55
+
56
+ // 更新 token
57
+ data[userName] = {
58
+ ...tokenInfo,
59
+ expiresAt: Date.now() + tokenInfo.expiresIn * 1000,
60
+ };
61
+
62
+ // 写入文件(仅当前用户可读写)
63
+ fs.writeFileSync(TOKEN_FILE, JSON.stringify(data, null, 2), { mode: 0o600 });
64
+ log(`Token saved locally for user: ${userName}`);
65
+ } catch (e) {
66
+ log('Failed to save local token:', e.message);
67
+ }
68
+ }
69
+
70
+ // 检查 token 是否即将过期(提前 5 分钟刷新)
71
+ function isTokenExpiringSoon(tokenInfo) {
72
+ if (!tokenInfo || !tokenInfo.expiresAt) return true;
73
+ return tokenInfo.expiresAt < Date.now() + 5 * 60 * 1000;
74
+ }
75
+
76
+ // 刷新本地 token
77
+ async function refreshLocalToken() {
78
+ const tokenInfo = loadLocalToken(USER_NAME);
79
+ if (!tokenInfo || !tokenInfo.refreshToken) {
80
+ log('No refresh token available');
81
+ return false;
82
+ }
83
+
84
+ try {
85
+ const res = await fetch(`${SERVER_URL}/refresh-token`, {
86
+ method: 'POST',
87
+ headers: { 'Content-Type': 'application/json' },
88
+ body: JSON.stringify({ refresh_token: tokenInfo.refreshToken }),
89
+ });
90
+
91
+ if (!res.ok) {
92
+ const error = await res.json();
93
+ throw new Error(error.error || 'Refresh failed');
94
+ }
95
+
96
+ const newToken = await res.json();
97
+ saveLocalToken(USER_NAME, {
98
+ ...tokenInfo,
99
+ accessToken: newToken.accessToken,
100
+ refreshToken: newToken.refreshToken,
101
+ expiresIn: newToken.expiresIn,
102
+ });
103
+ log('Token refreshed successfully');
104
+ return true;
105
+ } catch (e) {
106
+ log('Token refresh failed:', e.message);
107
+ return false;
108
+ }
109
+ }
110
+
111
+ // 存储 messages endpoint 和授权 URL
112
+ let messagesEndpoint = null;
113
+ let authUrl = null;
114
+ let sseConnected = false;
115
+ let pendingRequests = new Map();
116
+
117
+ function normalizeEndpoint(endpoint) {
118
+ try {
119
+ const url = new URL(endpoint);
120
+ const isLocal = url.hostname === 'localhost' || url.hostname === '127.0.0.1';
121
+ if (!isLocal && url.protocol === 'http:') {
122
+ url.protocol = 'https:';
123
+ return url.toString();
124
+ }
125
+ } catch {
126
+ // keep original endpoint if parsing fails
127
+ }
128
+ return endpoint;
129
+ }
130
+
131
+ // 连接 SSE
132
+ function connectSSE() {
133
+ const sseUrl = `${SERVER_URL}/mcp?device_id=${USER_NAME}`;
134
+ log(`Connecting to ${sseUrl}`);
135
+
136
+ const es = new EventSource(sseUrl);
137
+
138
+ es.addEventListener('endpoint', (event) => {
139
+ messagesEndpoint = normalizeEndpoint(event.data);
140
+ log(`Got endpoint: ${messagesEndpoint}`);
141
+ sseConnected = true;
142
+ });
143
+
144
+ // 监听 token 事件(OAuth 回调后服务器推送)
145
+ es.addEventListener('token', (event) => {
146
+ try {
147
+ const tokenInfo = JSON.parse(event.data);
148
+ saveLocalToken(USER_NAME, tokenInfo);
149
+ log(`Received and saved token for user: ${tokenInfo.name}`);
150
+ } catch (e) {
151
+ log('Token event parse error:', e.message);
152
+ }
153
+ });
154
+
155
+ es.addEventListener('notification', (event) => {
156
+ try {
157
+ const data = JSON.parse(event.data);
158
+ // 保存授权 URL
159
+ if (data.auth_url) {
160
+ authUrl = data.auth_url;
161
+ }
162
+
163
+ if (data.type === 'auth_required' && data.auth_url) {
164
+ log(`Opening browser for auth: ${data.auth_url}`);
165
+ open(data.auth_url);
166
+ } else if (data.type === 'connected') {
167
+ // 检查本地是否有 token
168
+ const localToken = loadLocalToken(USER_NAME);
169
+ if (!localToken) {
170
+ log(`No local token found, opening auth: ${data.auth_url}`);
171
+ if (data.auth_url) {
172
+ open(data.auth_url);
173
+ }
174
+ } else {
175
+ log(`Local token found, ready to use`);
176
+ }
177
+ } else if (data.type === 'auth_success') {
178
+ log(`Auth success: ${data.message}`);
179
+ }
180
+ } catch (e) {
181
+ log('Notification parse error:', e.message);
182
+ }
183
+ });
184
+
185
+ es.addEventListener('message', (event) => {
186
+ try {
187
+ const response = JSON.parse(event.data);
188
+ // 发送响应到 stdout
189
+ const output = JSON.stringify(response);
190
+ process.stdout.write(output + '\n');
191
+ log(`Response sent for id=${response.id}`);
192
+ } catch (e) {
193
+ log('Message parse error:', e.message);
194
+ }
195
+ });
196
+
197
+ es.onerror = (err) => {
198
+ log('SSE error, reconnecting...');
199
+ sseConnected = false;
200
+ es.close();
201
+ setTimeout(connectSSE, 3000);
202
+ };
203
+ }
204
+
205
+ // 发送请求到服务器
206
+ async function sendRequest(request) {
207
+ if (!messagesEndpoint) {
208
+ log('Waiting for endpoint...');
209
+ await new Promise(resolve => {
210
+ const check = setInterval(() => {
211
+ if (messagesEndpoint) {
212
+ clearInterval(check);
213
+ resolve();
214
+ }
215
+ }, 100);
216
+ });
217
+ }
218
+
219
+ // 获取本地 token
220
+ let tokenInfo = loadLocalToken(USER_NAME);
221
+
222
+ // 检查是否需要刷新 token
223
+ if (tokenInfo && isTokenExpiringSoon(tokenInfo)) {
224
+ log('Token expiring soon, refreshing...');
225
+ const refreshed = await refreshLocalToken();
226
+ if (refreshed) {
227
+ tokenInfo = loadLocalToken(USER_NAME);
228
+ }
229
+ }
230
+
231
+ // 如果没有 token 且不是初始化请求,触发授权
232
+ const initPhaseMethods = ['initialize', 'notifications/initialized', 'initialized', 'tools/list'];
233
+ if (!tokenInfo && !initPhaseMethods.includes(request.method)) {
234
+ if (authUrl) {
235
+ log(`No token available, opening auth: ${authUrl}`);
236
+ open(authUrl);
237
+ }
238
+ }
239
+
240
+ try {
241
+ const headers = { 'Content-Type': 'application/json' };
242
+ if (tokenInfo?.accessToken) {
243
+ headers['Authorization'] = `Bearer ${tokenInfo.accessToken}`;
244
+ }
245
+
246
+ const response = await fetch(messagesEndpoint, {
247
+ method: 'POST',
248
+ headers,
249
+ body: JSON.stringify(request),
250
+ });
251
+
252
+ if (response.status === 202) {
253
+ // 响应会通过 SSE 返回
254
+ log(`Request sent: ${request.method}, id=${request.id}`);
255
+ } else {
256
+ const text = await response.text();
257
+ log(`Unexpected response: ${response.status} ${text}`);
258
+ }
259
+ } catch (e) {
260
+ log('Request error:', e.message);
261
+ // 返回错误响应
262
+ const errorResponse = {
263
+ jsonrpc: '2.0',
264
+ id: request.id,
265
+ error: { code: -32000, message: e.message }
266
+ };
267
+ process.stdout.write(JSON.stringify(errorResponse) + '\n');
268
+ }
269
+ }
270
+
271
+ // 读取 stdin
272
+ let buffer = '';
273
+ process.stdin.setEncoding('utf8');
274
+ process.stdin.on('data', (chunk) => {
275
+ buffer += chunk;
276
+
277
+ // 尝试解析完整的 JSON 消息
278
+ let newlineIndex;
279
+ while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
280
+ const line = buffer.slice(0, newlineIndex);
281
+ buffer = buffer.slice(newlineIndex + 1);
282
+
283
+ if (line.trim()) {
284
+ try {
285
+ const request = JSON.parse(line);
286
+ log(`Received: ${request.method}, id=${request.id}`);
287
+ sendRequest(request);
288
+ } catch (e) {
289
+ log('Parse error:', e.message);
290
+ }
291
+ }
292
+ }
293
+ });
294
+
295
+ process.stdin.on('end', () => {
296
+ log('stdin closed, exiting');
297
+ process.exit(0);
298
+ });
299
+
300
+ // 启动
301
+ log(`Starting proxy for user: ${USER_NAME}`);
302
+ log(`Server: ${SERVER_URL}`);
303
+ connectSSE();
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "lark-mcp-server",
3
+ "version": "0.0.1",
4
+ "description": "飞书 MCP 本地代理 - 让 AI 编程工具读取飞书文档,Token 本地安全存储",
5
+ "type": "module",
6
+ "bin": {
7
+ "lark-mcp-server": "./index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "README.md"
12
+ ],
13
+ "keywords": [
14
+ "lark",
15
+ "feishu",
16
+ "mcp",
17
+ "proxy",
18
+ "claude",
19
+ "cursor",
20
+ "ai",
21
+ "document"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/wuzhihua/lark-mcp-proxy"
26
+ },
27
+ "author": "wuzhihua",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "eventsource": "^2.0.2",
31
+ "open": "^10.1.0"
32
+ }
33
+ }