chaimi-keep-mcp 3.1.24
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/.env.example +26 -0
- package/README.md +222 -0
- package/bin/cli.js +233 -0
- package/bin/get-auth-code.js +92 -0
- package/config.example.yaml +15 -0
- package/install.sh +268 -0
- package/oauth.js +493 -0
- package/package.json +42 -0
- package/server.js +1064 -0
package/oauth.js
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Device Flow 管理器
|
|
3
|
+
* 实现 OAuth 2.0 Device Authorization Grant
|
|
4
|
+
* 支持双模式:URL Scheme(浏览器环境)/ 纯 Device Flow(CLI环境)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fetch = require('node-fetch');
|
|
8
|
+
const { exec } = require('child_process');
|
|
9
|
+
const util = require('util');
|
|
10
|
+
const execPromise = util.promisify(exec);
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
|
|
13
|
+
class OAuthManager {
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.mcpOAuthUrl = config.mcpOAuthUrl;
|
|
16
|
+
this.tokenStorage = config.tokenStorage;
|
|
17
|
+
this.onQrCode = config.onQrCode;
|
|
18
|
+
this.onTokenReady = config.onTokenReady;
|
|
19
|
+
this.preferUrlScheme = config.preferUrlScheme !== false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async detectEnvironment() {
|
|
23
|
+
if (process.env.DISPLAY) {
|
|
24
|
+
return { supportsBrowser: true, platform: 'linux-gui' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (process.platform === 'darwin') {
|
|
28
|
+
return { supportsBrowser: true, platform: 'macos' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (process.platform === 'win32') {
|
|
32
|
+
return { supportsBrowser: true, platform: 'windows' };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (process.env.BROWSER) {
|
|
36
|
+
return { supportsBrowser: true, platform: 'custom' };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { supportsBrowser: false, platform: 'cli' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async startAuthFlow() {
|
|
43
|
+
try {
|
|
44
|
+
const env = await this.detectEnvironment();
|
|
45
|
+
const useUrlScheme = this.preferUrlScheme && env.supportsBrowser;
|
|
46
|
+
const deviceCodeRes = await this.requestDeviceCode(useUrlScheme);
|
|
47
|
+
|
|
48
|
+
await this.saveAuthState({
|
|
49
|
+
deviceCode: deviceCodeRes.deviceCode,
|
|
50
|
+
userCode: deviceCodeRes.userCode,
|
|
51
|
+
timestamp: Date.now(),
|
|
52
|
+
status: 'pending'
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (useUrlScheme && deviceCodeRes.urlScheme) {
|
|
56
|
+
await this.authorizeWithUrlScheme(deviceCodeRes);
|
|
57
|
+
} else {
|
|
58
|
+
await this.authorizeWithDeviceFlow(deviceCodeRes);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. 轮询获取 Token
|
|
62
|
+
// 注意:使用 console.error,避免污染 stdout(MCP 协议通信使用 stdout)
|
|
63
|
+
console.error('');
|
|
64
|
+
console.error('⏳ 等待用户授权,请勿关闭窗口...');
|
|
65
|
+
console.error(' (请在手机微信中完成授权操作)');
|
|
66
|
+
console.error('');
|
|
67
|
+
const token = await this.pollForToken(
|
|
68
|
+
deviceCodeRes.deviceCode,
|
|
69
|
+
deviceCodeRes.interval * 1000 || 5000
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
await this.tokenStorage.save(token);
|
|
73
|
+
|
|
74
|
+
if (this.onTokenReady) {
|
|
75
|
+
this.onTokenReady(token);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return token;
|
|
79
|
+
} catch (err) {
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async authorizeWithUrlScheme(deviceCodeRes) {
|
|
85
|
+
const isMobile = this.detectMobileEnvironment();
|
|
86
|
+
|
|
87
|
+
if (isMobile) {
|
|
88
|
+
const url = deviceCodeRes.urlScheme || deviceCodeRes.verificationUriFull;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await this.openBrowser(url);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
const qrCodeUrl = `https://mcp.chaihuo.com/auth?deviceCode=${deviceCodeRes.deviceCode}&userCode=${deviceCodeRes.userCode}`;
|
|
96
|
+
|
|
97
|
+
if (this.onQrCode) {
|
|
98
|
+
await this.onQrCode({
|
|
99
|
+
userCode: deviceCodeRes.userCode,
|
|
100
|
+
verificationUri: deviceCodeRes.verificationUri,
|
|
101
|
+
deviceCode: deviceCodeRes.deviceCode,
|
|
102
|
+
qrCodeUrl: qrCodeUrl,
|
|
103
|
+
isPC: true
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
detectMobileEnvironment() {
|
|
110
|
+
const userAgent = process.env.USER_AGENT || '';
|
|
111
|
+
if (/iPhone|iPad|iPod|Android|Mobile/i.test(userAgent)) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (process.env.TERM_PROGRAM === 'Apple_Terminal' && process.platform === 'darwin') {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async authorizeWithDeviceFlow(deviceCodeRes) {
|
|
123
|
+
if (this.onQrCode) {
|
|
124
|
+
await this.onQrCode({
|
|
125
|
+
userCode: deviceCodeRes.userCode,
|
|
126
|
+
verificationUri: deviceCodeRes.verificationUri,
|
|
127
|
+
deviceCode: deviceCodeRes.deviceCode
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async openBrowser(url) {
|
|
133
|
+
const platform = process.platform;
|
|
134
|
+
let command;
|
|
135
|
+
|
|
136
|
+
if (platform === 'darwin') {
|
|
137
|
+
command = `open "${url}"`;
|
|
138
|
+
} else if (platform === 'win32') {
|
|
139
|
+
command = `start "" "${url}"`;
|
|
140
|
+
} else if (platform === 'linux') {
|
|
141
|
+
command = `xdg-open "${url}"`;
|
|
142
|
+
} else {
|
|
143
|
+
throw new Error('不支持的平台');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await execPromise(command);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async requestDeviceCode(useUrlScheme = false) {
|
|
150
|
+
const response = await fetch(this.mcpOAuthUrl, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: {
|
|
153
|
+
'Content-Type': 'application/json'
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
tool: 'deviceCode',
|
|
157
|
+
params: {
|
|
158
|
+
clientId: 'chaihuo-mcp-client',
|
|
159
|
+
useUrlScheme: useUrlScheme
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const result = await response.json();
|
|
165
|
+
|
|
166
|
+
if (!result.success) {
|
|
167
|
+
throw new Error(`获取设备码失败: ${result.error}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return result.data;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 单次轮询(用于后台轮询)
|
|
174
|
+
async pollForTokenOnce(deviceCode) {
|
|
175
|
+
try {
|
|
176
|
+
const response = await fetch(this.mcpOAuthUrl, {
|
|
177
|
+
method: 'POST',
|
|
178
|
+
headers: {
|
|
179
|
+
'Content-Type': 'application/json'
|
|
180
|
+
},
|
|
181
|
+
body: JSON.stringify({
|
|
182
|
+
tool: 'deviceToken',
|
|
183
|
+
params: {
|
|
184
|
+
deviceCode
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const result = await response.json();
|
|
190
|
+
|
|
191
|
+
if (result.success) {
|
|
192
|
+
return {
|
|
193
|
+
accessToken: result.data.accessToken,
|
|
194
|
+
refreshToken: result.data.refreshToken,
|
|
195
|
+
expiresIn: result.data.expiresIn,
|
|
196
|
+
tokenType: result.data.tokenType,
|
|
197
|
+
expiresAt: Date.now() + result.data.expiresIn * 1000
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (result.error === 'authorization_pending') {
|
|
202
|
+
return null; // 还未授权,返回 null
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (result.error === 'expired_token') {
|
|
206
|
+
throw new Error('设备码已过期,请重新授权');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (result.error === 'invalid_device_code') {
|
|
210
|
+
throw new Error('无效的设备码');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
throw new Error(`获取 Token 失败: ${result.error}`);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
throw err;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async pollForToken(deviceCode, interval) {
|
|
220
|
+
const maxAttempts = 60;
|
|
221
|
+
let attempts = 0;
|
|
222
|
+
|
|
223
|
+
while (attempts < maxAttempts) {
|
|
224
|
+
attempts++;
|
|
225
|
+
|
|
226
|
+
await this.delay(interval);
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const token = await this.pollForTokenOnce(deviceCode);
|
|
230
|
+
if (token) {
|
|
231
|
+
return token;
|
|
232
|
+
}
|
|
233
|
+
// 未授权,继续轮询
|
|
234
|
+
} catch (err) {
|
|
235
|
+
if (err.message.includes('expired') || err.message.includes('invalid')) {
|
|
236
|
+
throw err;
|
|
237
|
+
}
|
|
238
|
+
// 其他错误,继续轮询
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
throw new Error('授权超时,请重新尝试');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async refreshToken() {
|
|
246
|
+
const currentToken = await this.tokenStorage.load();
|
|
247
|
+
|
|
248
|
+
if (!currentToken || !currentToken.refreshToken) {
|
|
249
|
+
throw new Error('没有可用的 Refresh Token,需要重新授权');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const response = await fetch(this.mcpOAuthUrl, {
|
|
254
|
+
method: 'POST',
|
|
255
|
+
headers: {
|
|
256
|
+
'Content-Type': 'application/json'
|
|
257
|
+
},
|
|
258
|
+
body: JSON.stringify({
|
|
259
|
+
tool: 'refreshToken',
|
|
260
|
+
params: {
|
|
261
|
+
refreshToken: currentToken.refreshToken
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const result = await response.json();
|
|
267
|
+
|
|
268
|
+
if (!result.success) {
|
|
269
|
+
if (result.error === 'refresh_token_expired') {
|
|
270
|
+
throw new Error('Refresh Token 已过期,需要重新授权');
|
|
271
|
+
}
|
|
272
|
+
throw new Error(`刷新 Token 失败: ${result.error}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const newToken = {
|
|
276
|
+
accessToken: result.data.accessToken,
|
|
277
|
+
refreshToken: result.data.refreshToken,
|
|
278
|
+
expiresIn: result.data.expiresIn,
|
|
279
|
+
tokenType: result.data.tokenType,
|
|
280
|
+
expiresAt: Date.now() + result.data.expiresIn * 1000
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
await this.tokenStorage.save(newToken);
|
|
284
|
+
|
|
285
|
+
return newToken;
|
|
286
|
+
} catch (err) {
|
|
287
|
+
throw err;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async getValidToken() {
|
|
292
|
+
const token = await this.tokenStorage.load();
|
|
293
|
+
|
|
294
|
+
if (!token) {
|
|
295
|
+
throw new Error('没有存储的 Token,请先授权');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const refreshThreshold = 5 * 60 * 1000;
|
|
299
|
+
if (Date.now() + refreshThreshold > token.expiresAt) {
|
|
300
|
+
return await this.refreshToken();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return token;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
delay(ms) {
|
|
307
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async saveAuthState(state) {
|
|
311
|
+
try {
|
|
312
|
+
const fs = require('fs').promises;
|
|
313
|
+
const path = require('path');
|
|
314
|
+
const stateFile = path.join(require('os').homedir(), '.mcporter', 'auth-state.json');
|
|
315
|
+
|
|
316
|
+
const dir = path.dirname(stateFile);
|
|
317
|
+
await fs.mkdir(dir, { recursive: true });
|
|
318
|
+
|
|
319
|
+
await fs.writeFile(
|
|
320
|
+
stateFile,
|
|
321
|
+
JSON.stringify(state, null, 2),
|
|
322
|
+
{ mode: 0o600 }
|
|
323
|
+
);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async loadAuthState() {
|
|
329
|
+
try {
|
|
330
|
+
const fs = require('fs').promises;
|
|
331
|
+
const path = require('path');
|
|
332
|
+
const stateFile = path.join(require('os').homedir(), '.mcporter', 'auth-state.json');
|
|
333
|
+
|
|
334
|
+
const data = await fs.readFile(stateFile, 'utf8');
|
|
335
|
+
return JSON.parse(data);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
if (err.code === 'ENOENT') {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async clearAuthState() {
|
|
345
|
+
try {
|
|
346
|
+
const fs = require('fs').promises;
|
|
347
|
+
const path = require('path');
|
|
348
|
+
const stateFile = path.join(require('os').homedir(), '.mcporter', 'auth-state.json');
|
|
349
|
+
await fs.unlink(stateFile);
|
|
350
|
+
} catch (err) {
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
isAuthStateExpired(state, maxAge = 30 * 60 * 1000) {
|
|
355
|
+
// 优先检查 expiresAt(如果存在)
|
|
356
|
+
if (state && state.expiresAt) {
|
|
357
|
+
return Date.now() > state.expiresAt;
|
|
358
|
+
}
|
|
359
|
+
// 兼容旧版本,检查 timestamp
|
|
360
|
+
if (!state || !state.timestamp) return true;
|
|
361
|
+
return Date.now() - state.timestamp > maxAge;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
class TokenStorage {
|
|
366
|
+
async save(token) {
|
|
367
|
+
throw new Error('TokenStorage.save 必须被子类实现');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async load() {
|
|
371
|
+
throw new Error('TokenStorage.load 必须被子类实现');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async clear() {
|
|
375
|
+
throw new Error('TokenStorage.clear 必须被子类实现');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 生成机器唯一标识(用于加密密钥)
|
|
380
|
+
function getMachineId() {
|
|
381
|
+
const os = require('os');
|
|
382
|
+
const interfaces = os.networkInterfaces();
|
|
383
|
+
let macAddress = '';
|
|
384
|
+
|
|
385
|
+
// 获取第一个非本地 MAC 地址
|
|
386
|
+
for (const name of Object.keys(interfaces)) {
|
|
387
|
+
for (const iface of interfaces[name]) {
|
|
388
|
+
if (iface.mac && iface.mac !== '00:00:00:00:00:00') {
|
|
389
|
+
macAddress = iface.mac;
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (macAddress) break;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 结合主机名和平台信息生成密钥
|
|
397
|
+
const keyMaterial = `${macAddress}-${os.hostname()}-${os.platform()}`;
|
|
398
|
+
return crypto.createHash('sha256').update(keyMaterial).digest('hex').substring(0, 32);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// 加密 Token
|
|
402
|
+
function encryptToken(token, key) {
|
|
403
|
+
const iv = crypto.randomBytes(16);
|
|
404
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv);
|
|
405
|
+
let encrypted = cipher.update(JSON.stringify(token), 'utf8', 'hex');
|
|
406
|
+
encrypted += cipher.final('hex');
|
|
407
|
+
return iv.toString('hex') + ':' + encrypted;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// 解密 Token
|
|
411
|
+
function decryptToken(encryptedData, key) {
|
|
412
|
+
const parts = encryptedData.split(':');
|
|
413
|
+
const iv = Buffer.from(parts[0], 'hex');
|
|
414
|
+
const encrypted = parts[1];
|
|
415
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key), iv);
|
|
416
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
417
|
+
decrypted += decipher.final('utf8');
|
|
418
|
+
return JSON.parse(decrypted);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
class FileTokenStorage extends TokenStorage {
|
|
422
|
+
constructor(filePath) {
|
|
423
|
+
super();
|
|
424
|
+
this.filePath = filePath;
|
|
425
|
+
this.fs = require('fs').promises;
|
|
426
|
+
this.path = require('path');
|
|
427
|
+
this.key = getMachineId();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async save(token) {
|
|
431
|
+
const dir = this.path.dirname(this.filePath);
|
|
432
|
+
await this.fs.mkdir(dir, { recursive: true });
|
|
433
|
+
|
|
434
|
+
// 加密 Token 后保存
|
|
435
|
+
const encrypted = encryptToken(token, this.key);
|
|
436
|
+
const data = {
|
|
437
|
+
version: '2.0',
|
|
438
|
+
encrypted: encrypted,
|
|
439
|
+
algorithm: 'aes-256-cbc',
|
|
440
|
+
updatedAt: new Date().toISOString()
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
await this.fs.writeFile(
|
|
444
|
+
this.filePath,
|
|
445
|
+
JSON.stringify(data, null, 2),
|
|
446
|
+
{ mode: 0o600 }
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async load() {
|
|
451
|
+
try {
|
|
452
|
+
const data = await this.fs.readFile(this.filePath, 'utf8');
|
|
453
|
+
const parsed = JSON.parse(data);
|
|
454
|
+
|
|
455
|
+
// 向后兼容:检测旧版明文格式
|
|
456
|
+
if (!parsed.version || parsed.version === '1.0') {
|
|
457
|
+
console.error('检测到旧版 Token 格式,自动升级...');
|
|
458
|
+
// 返回明文数据,但下次保存时会自动加密
|
|
459
|
+
return {
|
|
460
|
+
accessToken: parsed.accessToken,
|
|
461
|
+
refreshToken: parsed.refreshToken,
|
|
462
|
+
expiresAt: parsed.expiresAt,
|
|
463
|
+
expiresIn: parsed.expiresIn,
|
|
464
|
+
tokenType: parsed.tokenType
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// 新版加密格式,解密后返回
|
|
469
|
+
return decryptToken(parsed.encrypted, this.key);
|
|
470
|
+
} catch (err) {
|
|
471
|
+
if (err.code === 'ENOENT') {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
throw err;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async clear() {
|
|
479
|
+
try {
|
|
480
|
+
await this.fs.unlink(this.filePath);
|
|
481
|
+
} catch (err) {
|
|
482
|
+
if (err.code !== 'ENOENT') {
|
|
483
|
+
throw err;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
module.exports = {
|
|
490
|
+
OAuthManager,
|
|
491
|
+
TokenStorage,
|
|
492
|
+
FileTokenStorage
|
|
493
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chaimi-keep-mcp",
|
|
3
|
+
"version": "3.1.24",
|
|
4
|
+
"description": "柴米记账 MCP Server - 支持 Claude、Cursor、OpenClaw、WorkBuddy 等 AI 工具直接记账",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"chaimi-keep-mcp": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "commonjs",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node server.js",
|
|
12
|
+
"dev": "node server.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"mcp",
|
|
16
|
+
"记账",
|
|
17
|
+
"微信小程序",
|
|
18
|
+
"openclaw",
|
|
19
|
+
"chaihuo",
|
|
20
|
+
"chaimi",
|
|
21
|
+
"AI记账",
|
|
22
|
+
"model-context-protocol"
|
|
23
|
+
],
|
|
24
|
+
"author": "柴米记账团队,songyangx@gmail.com",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
28
|
+
"dotenv": "^17.3.1",
|
|
29
|
+
"node-fetch": "^2.7.0"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18.0.0"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/yourusername/chaimi-bookkeeping-mcp.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/yourusername/chaimi-bookkeeping-mcp/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/yourusername/chaimi-bookkeeping-mcp#readme"
|
|
42
|
+
}
|