chaimi-bookkeeping-mcp 2.3.6 → 2.3.9

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 (4) hide show
  1. package/oauth.js +9 -192
  2. package/package.json +1 -1
  3. package/server.js +489 -85
  4. package/test-mcp.sh +26 -0
package/oauth.js CHANGED
@@ -13,74 +13,37 @@ class OAuthManager {
13
13
  constructor(config) {
14
14
  this.mcpOAuthUrl = config.mcpOAuthUrl;
15
15
  this.tokenStorage = config.tokenStorage;
16
- this.onQrCode = config.onQrCode; // 回调函数,显示二维码
17
- this.onTokenReady = config.onTokenReady; // 回调函数,Token就绪
18
- this.preferUrlScheme = config.preferUrlScheme !== false; // 默认优先使用 URL Scheme
16
+ this.onQrCode = config.onQrCode;
17
+ this.onTokenReady = config.onTokenReady;
18
+ this.preferUrlScheme = config.preferUrlScheme !== false;
19
19
  }
20
20
 
21
- /**
22
- * 检测当前环境是否支持浏览器/URL Scheme
23
- */
24
21
  async detectEnvironment() {
25
- // 检测是否有 DISPLAY 环境变量(Linux GUI)
26
22
  if (process.env.DISPLAY) {
27
23
  return { supportsBrowser: true, platform: 'linux-gui' };
28
24
  }
29
25
 
30
- // 检测是否为 macOS
31
26
  if (process.platform === 'darwin') {
32
27
  return { supportsBrowser: true, platform: 'macos' };
33
28
  }
34
29
 
35
- // 检测是否为 Windows
36
30
  if (process.platform === 'win32') {
37
31
  return { supportsBrowser: true, platform: 'windows' };
38
32
  }
39
33
 
40
- // 检测是否有 BROWSER 环境变量
41
34
  if (process.env.BROWSER) {
42
35
  return { supportsBrowser: true, platform: 'custom' };
43
36
  }
44
37
 
45
- // 默认认为不支持浏览器(纯 CLI 环境如 Claude Desktop)
46
38
  return { supportsBrowser: false, platform: 'cli' };
47
39
  }
48
40
 
49
- /**
50
- * 启动 OAuth 授权流程
51
- * 自动检测环境,选择最佳交互方式
52
- */
53
41
  async startAuthFlow() {
54
42
  try {
55
- console.log('\n');
56
- console.log('╔════════════════════════════════════════════════════════╗');
57
- console.log('║ 🔴 【重要】首次使用需要完成 OAuth 授权 ║');
58
- console.log('║ ⏱️ 授权流程约需 1-2 分钟,请耐心等待 ║');
59
- console.log('╚════════════════════════════════════════════════════════╝');
60
- console.log('\n');
61
-
62
- // 检测环境
63
43
  const env = await this.detectEnvironment();
64
- console.log(`检测到环境: ${env.platform} (${env.supportsBrowser ? '支持浏览器' : '纯终端'})`);
65
-
66
- // 1. 获取设备码(根据环境决定是否请求 URL Scheme)
67
44
  const useUrlScheme = this.preferUrlScheme && env.supportsBrowser;
68
45
  const deviceCodeRes = await this.requestDeviceCode(useUrlScheme);
69
46
 
70
- console.log('✅ 获取设备码成功');
71
- console.log('');
72
-
73
- // 【柴米记账授权】格式化输出,确保Agent能识别
74
- const authMessage = `
75
- 【柴米记账授权】
76
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
77
- 📱 验证码: ${deviceCodeRes.userCode}
78
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
79
- 请在微信柴米记账小程序中输入此验证码完成授权
80
- `;
81
- console.log(authMessage);
82
-
83
- // 保存授权状态,支持进程重启后复用
84
47
  await this.saveAuthState({
85
48
  deviceCode: deviceCodeRes.deviceCode,
86
49
  userCode: deviceCodeRes.userCode,
@@ -88,88 +51,42 @@ class OAuthManager {
88
51
  status: 'pending'
89
52
  });
90
53
 
91
- // 2. 根据环境选择交互方式
92
54
  if (useUrlScheme && deviceCodeRes.urlScheme) {
93
- // 方式A:URL Scheme(浏览器环境)
94
55
  await this.authorizeWithUrlScheme(deviceCodeRes);
95
56
  } else {
96
- // 方式B:纯 Device Flow(CLI环境)
97
57
  await this.authorizeWithDeviceFlow(deviceCodeRes);
98
58
  }
99
59
 
100
- // 3. 轮询获取 Token
101
- console.log('');
102
- console.log('⏳ 等待用户授权,请勿关闭窗口...');
103
- console.log(' (请在手机微信中完成授权操作)');
104
- console.log(' 授权完成后会自动继续,无需手动告知');
105
- console.log('');
106
60
  const token = await this.pollForToken(
107
61
  deviceCodeRes.deviceCode,
108
- 2000 // 固定2秒轮询,更快响应
62
+ 2000
109
63
  );
110
64
 
111
- // 4. 存储 Token
112
65
  await this.tokenStorage.save(token);
113
66
 
114
- console.log('');
115
- console.log('╔════════════════════════════════════════════════════════╗');
116
- console.log('║ ✅ 授权成功!现在可以开始使用柴米记账 MCP Server ║');
117
- console.log('╚════════════════════════════════════════════════════════╝');
118
- console.log('');
119
-
120
67
  if (this.onTokenReady) {
121
68
  this.onTokenReady(token);
122
69
  }
123
70
 
124
71
  return token;
125
72
  } catch (err) {
126
- console.error('\n❌ 授权失败:', err.message);
127
- console.log('========================================\n');
128
73
  throw err;
129
74
  }
130
75
  }
131
76
 
132
- /**
133
- * 使用 URL Scheme 方式授权(浏览器环境)
134
- * PC端:显示二维码 + 验证码
135
- * 手机端:打开跳转链接
136
- */
137
77
  async authorizeWithUrlScheme(deviceCodeRes) {
138
- // 检测是否为移动端环境
139
78
  const isMobile = this.detectMobileEnvironment();
140
79
 
141
80
  if (isMobile) {
142
- // 手机端:打开 URL Scheme 跳转链接
143
- console.log('\n📱 请在微信中完成授权');
144
- console.log('正在尝试自动打开微信...\n');
145
-
146
81
  const url = deviceCodeRes.urlScheme || deviceCodeRes.verificationUriFull;
147
82
 
148
83
  try {
149
84
  await this.openBrowser(url);
150
- console.log('✅ 已尝试打开微信');
151
- console.log('请在微信中完成授权\n');
152
85
  } catch (err) {
153
- console.log('⚠️ 无法自动打开微信,请手动操作:');
154
- console.log(`1. 复制链接: ${url}`);
155
- console.log('2. 在微信中打开该链接\n');
156
86
  }
157
87
  } else {
158
- // PC端:显示二维码 + 验证码
159
- console.log('\n💻 请在手机微信中完成授权');
160
- console.log('========================================');
161
- console.log('');
162
-
163
- // 生成小程序二维码链接
164
88
  const qrCodeUrl = `https://mcp.chaihuo.com/auth?deviceCode=${deviceCodeRes.deviceCode}&userCode=${deviceCodeRes.userCode}`;
165
89
 
166
- // 显示二维码(使用 ASCII 艺术或链接)
167
- console.log('请使用微信扫描下方二维码:');
168
- console.log('');
169
- console.log(` ${qrCodeUrl}`);
170
- console.log('');
171
-
172
- // 如果有二维码回调,显示图形二维码
173
90
  if (this.onQrCode) {
174
91
  await this.onQrCode({
175
92
  userCode: deviceCodeRes.userCode,
@@ -179,81 +96,41 @@ class OAuthManager {
179
96
  isPC: true
180
97
  });
181
98
  }
182
-
183
- console.log('========================================');
184
- console.log('');
185
- console.log('或者手动输入验证码:');
186
- console.log(` 验证码: ${deviceCodeRes.userCode}`);
187
- console.log('');
188
- console.log('操作步骤:');
189
- console.log('1. 打开微信,进入"柴米记账"小程序');
190
- console.log('2. 点击"我的" → "🤖 Agent 授权"');
191
- console.log(`3. 输入验证码: ${deviceCodeRes.userCode}`);
192
- console.log('4. 点击"确认授权"');
193
- console.log('');
194
99
  }
195
100
  }
196
101
 
197
- /**
198
- * 检测是否为移动端环境
199
- */
200
102
  detectMobileEnvironment() {
201
- // 检测 User Agent
202
103
  const userAgent = process.env.USER_AGENT || '';
203
104
  if (/iPhone|iPad|iPod|Android|Mobile/i.test(userAgent)) {
204
105
  return true;
205
106
  }
206
107
 
207
- // 检测是否有移动端特定的环境变量
208
108
  if (process.env.TERM_PROGRAM === 'Apple_Terminal' && process.platform === 'darwin') {
209
- // macOS 终端,可能是 iTerm2 等,需要进一步判断
210
- // 这里假设 PC 端
211
109
  return false;
212
110
  }
213
111
 
214
- // 默认认为是 PC 端(保守策略)
215
- // 因为 PC 端显示二维码是通用的,手机端打开链接需要确认是移动端
216
112
  return false;
217
113
  }
218
114
 
219
- /**
220
- * 使用纯 Device Flow 方式授权(CLI环境)
221
- */
222
115
  async authorizeWithDeviceFlow(deviceCodeRes) {
223
- console.log(`\n验证地址: ${deviceCodeRes.verificationUri}\n`);
224
-
225
- // 显示二维码(如果提供了回调)
226
116
  if (this.onQrCode) {
227
117
  await this.onQrCode({
228
118
  userCode: deviceCodeRes.userCode,
229
119
  verificationUri: deviceCodeRes.verificationUri,
230
120
  deviceCode: deviceCodeRes.deviceCode
231
121
  });
232
- } else {
233
- // 默认输出到控制台
234
- console.log('请按以下步骤完成授权:');
235
- console.log('1. 打开手机微信,搜索小程序"柴米记账"');
236
- console.log('2. 进入"设置" → "授权管理"');
237
- console.log(`3. 输入验证码: ${deviceCodeRes.userCode}`);
238
- console.log('4. 点击"确认授权"\n');
239
122
  }
240
123
  }
241
124
 
242
- /**
243
- * 尝试打开浏览器
244
- */
245
125
  async openBrowser(url) {
246
126
  const platform = process.platform;
247
127
  let command;
248
128
 
249
129
  if (platform === 'darwin') {
250
- // macOS
251
130
  command = `open "${url}"`;
252
131
  } else if (platform === 'win32') {
253
- // Windows
254
132
  command = `start "" "${url}"`;
255
133
  } else if (platform === 'linux') {
256
- // Linux
257
134
  command = `xdg-open "${url}"`;
258
135
  } else {
259
136
  throw new Error('不支持的平台');
@@ -262,10 +139,6 @@ class OAuthManager {
262
139
  await execPromise(command);
263
140
  }
264
141
 
265
- /**
266
- * 请求设备码
267
- * @param {boolean} useUrlScheme - 是否请求 URL Scheme
268
- */
269
142
  async requestDeviceCode(useUrlScheme = false) {
270
143
  const response = await fetch(this.mcpOAuthUrl, {
271
144
  method: 'POST',
@@ -290,17 +163,13 @@ class OAuthManager {
290
163
  return result.data;
291
164
  }
292
165
 
293
- /**
294
- * 轮询获取 Token
295
- */
296
166
  async pollForToken(deviceCode, interval) {
297
- const maxAttempts = 60; // 最多轮询60次(5分钟)
167
+ const maxAttempts = 60;
298
168
  let attempts = 0;
299
169
 
300
170
  while (attempts < maxAttempts) {
301
171
  attempts++;
302
172
 
303
- // 等待间隔时间
304
173
  await this.delay(interval);
305
174
 
306
175
  try {
@@ -320,7 +189,6 @@ class OAuthManager {
320
189
  const result = await response.json();
321
190
 
322
191
  if (result.success) {
323
- // 获取 Token 成功
324
192
  return {
325
193
  accessToken: result.data.accessToken,
326
194
  refreshToken: result.data.refreshToken,
@@ -330,11 +198,7 @@ class OAuthManager {
330
198
  };
331
199
  }
332
200
 
333
- // 处理错误
334
201
  if (result.error === 'authorization_pending') {
335
- // 仍在等待授权,继续轮询
336
- const elapsedSeconds = attempts * (interval / 1000);
337
- process.stdout.write(`\r 等待中... ${elapsedSeconds}秒(请在小程完成授权)`);
338
202
  continue;
339
203
  }
340
204
 
@@ -351,17 +215,12 @@ class OAuthManager {
351
215
  if (err.message.includes('expired') || err.message.includes('invalid')) {
352
216
  throw err;
353
217
  }
354
- // 网络错误,继续轮询
355
- console.warn(`轮询出错: ${err.message},继续尝试...`);
356
218
  }
357
219
  }
358
220
 
359
221
  throw new Error('授权超时,请重新尝试');
360
222
  }
361
223
 
362
- /**
363
- * 刷新 Access Token
364
- */
365
224
  async refreshToken() {
366
225
  const currentToken = await this.tokenStorage.load();
367
226
 
@@ -392,7 +251,6 @@ class OAuthManager {
392
251
  throw new Error(`刷新 Token 失败: ${result.error}`);
393
252
  }
394
253
 
395
- // 更新 Token
396
254
  const newToken = {
397
255
  accessToken: result.data.accessToken,
398
256
  refreshToken: result.data.refreshToken,
@@ -403,19 +261,12 @@ class OAuthManager {
403
261
 
404
262
  await this.tokenStorage.save(newToken);
405
263
 
406
- console.log('✅ Token 刷新成功');
407
-
408
264
  return newToken;
409
265
  } catch (err) {
410
- console.error('刷新 Token 失败:', err.message);
411
266
  throw err;
412
267
  }
413
268
  }
414
269
 
415
- /**
416
- * 获取有效的 Access Token
417
- * 如果即将过期,自动刷新
418
- */
419
270
  async getValidToken() {
420
271
  const token = await this.tokenStorage.load();
421
272
 
@@ -423,53 +274,36 @@ class OAuthManager {
423
274
  throw new Error('没有存储的 Token,请先授权');
424
275
  }
425
276
 
426
- // 检查是否即将过期(提前5分钟刷新)
427
- const refreshThreshold = 5 * 60 * 1000; // 5分钟
277
+ const refreshThreshold = 5 * 60 * 1000;
428
278
  if (Date.now() + refreshThreshold > token.expiresAt) {
429
- console.log('Token 即将过期,自动刷新...');
430
279
  return await this.refreshToken();
431
280
  }
432
281
 
433
282
  return token;
434
283
  }
435
284
 
436
- /**
437
- * 延迟函数
438
- */
439
285
  delay(ms) {
440
286
  return new Promise(resolve => setTimeout(resolve, ms));
441
287
  }
442
288
 
443
- /**
444
- * 保存授权状态到文件
445
- * 支持进程重启后复用验证码
446
- */
447
289
  async saveAuthState(state) {
448
290
  try {
449
291
  const fs = require('fs').promises;
450
292
  const path = require('path');
451
293
  const stateFile = path.join(require('os').homedir(), '.mcporter', 'auth-state.json');
452
294
 
453
- // 确保目录存在
454
295
  const dir = path.dirname(stateFile);
455
296
  await fs.mkdir(dir, { recursive: true });
456
297
 
457
- // 保存状态
458
298
  await fs.writeFile(
459
299
  stateFile,
460
300
  JSON.stringify(state, null, 2),
461
301
  { mode: 0o600 }
462
302
  );
463
303
  } catch (err) {
464
- // 保存失败不影响主流程
465
- console.warn('保存授权状态失败:', err.message);
466
304
  }
467
305
  }
468
306
 
469
- /**
470
- * 加载授权状态
471
- * 用于进程重启后恢复授权流程
472
- */
473
307
  async loadAuthState() {
474
308
  try {
475
309
  const fs = require('fs').promises;
@@ -480,17 +314,12 @@ class OAuthManager {
480
314
  return JSON.parse(data);
481
315
  } catch (err) {
482
316
  if (err.code === 'ENOENT') {
483
- return null; // 文件不存在
317
+ return null;
484
318
  }
485
- console.warn('加载授权状态失败:', err.message);
486
319
  return null;
487
320
  }
488
321
  }
489
322
 
490
- /**
491
- * 清除授权状态
492
- * 授权成功或过期后清理
493
- */
494
323
  async clearAuthState() {
495
324
  try {
496
325
  const fs = require('fs').promises;
@@ -498,24 +327,15 @@ class OAuthManager {
498
327
  const stateFile = path.join(require('os').homedir(), '.mcporter', 'auth-state.json');
499
328
  await fs.unlink(stateFile);
500
329
  } catch (err) {
501
- // 文件不存在或其他错误,忽略
502
330
  }
503
331
  }
504
332
 
505
- /**
506
- * 检查授权状态是否过期
507
- * 默认10分钟有效期
508
- */
509
333
  isAuthStateExpired(state, maxAge = 10 * 60 * 1000) {
510
334
  if (!state || !state.timestamp) return true;
511
335
  return Date.now() - state.timestamp > maxAge;
512
336
  }
513
337
  }
514
338
 
515
- /**
516
- * Token 存储接口(基类)
517
- * 子类需要实现 save 和 load 方法
518
- */
519
339
  class TokenStorage {
520
340
  async save(token) {
521
341
  throw new Error('TokenStorage.save 必须被子类实现');
@@ -530,9 +350,6 @@ class TokenStorage {
530
350
  }
531
351
  }
532
352
 
533
- /**
534
- * 文件存储实现
535
- */
536
353
  class FileTokenStorage extends TokenStorage {
537
354
  constructor(filePath) {
538
355
  super();
@@ -547,7 +364,7 @@ class FileTokenStorage extends TokenStorage {
547
364
  await this.fs.writeFile(
548
365
  this.filePath,
549
366
  JSON.stringify(token, null, 2),
550
- { mode: 0o600 } // 仅所有者可读写
367
+ { mode: 0o600 }
551
368
  );
552
369
  }
553
370
 
@@ -557,7 +374,7 @@ class FileTokenStorage extends TokenStorage {
557
374
  return JSON.parse(data);
558
375
  } catch (err) {
559
376
  if (err.code === 'ENOENT') {
560
- return null; // 文件不存在
377
+ return null;
561
378
  }
562
379
  throw err;
563
380
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chaimi-bookkeeping-mcp",
3
- "version": "2.3.6",
3
+ "version": "2.3.9",
4
4
  "description": "柴米记账 MCP Server - 支持 Claude、Cursor、OpenClaw、WorkBuddy 等 AI 工具直接记账",
5
5
  "main": "server.js",
6
6
  "bin": {
package/server.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * 柴米记账 MCP Server (Node.js 版本)
4
+ * 适配微信云函数 mcpHub
4
5
  * 支持 Claude Desktop、Cursor、WorkBuddy、OpenClaw
5
6
  */
6
7
 
@@ -17,20 +18,51 @@ const {
17
18
  CallToolRequestSchema,
18
19
  ListToolsRequestSchema,
19
20
  } = require('@modelcontextprotocol/sdk/types.js');
20
- const fs = require('fs');
21
21
  const path = require('path');
22
+ const os = require('os');
23
+ const fs = require('fs');
22
24
 
23
- // 读取 package.json 获取版本号
25
+ // 读取 package.json 获取版本
24
26
  const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
25
27
  const MCP_VERSION = packageJson.version;
26
28
 
27
- // 配置
28
- const SCF_URL = process.env.SCF_URL || 'https://1412172089-4wbwsop8pe.ap-shanghai.tencentscf.com';
29
+ // 导入 OAuth 模块
30
+ const { OAuthManager, FileTokenStorage } = require('./oauth.js');
31
+
32
+ // 配置 - 微信云函数
33
+ const MCP_HUB_URL = process.env.MCP_HUB_URL || 'https://cloud1-2gfe5jhjef06b85d-1412172089.ap-shanghai.app.tcloudbase.com/mcpHub-mcp';
34
+ const MCP_PROMPT_URL = process.env.MCP_PROMPT_URL || 'https://cloud1-2gfe5jhjef06b85d-1412172089.ap-shanghai.app.tcloudbase.com/mcpPrompt';
35
+ const MCP_OAUTH_URL = process.env.MCP_OAUTH_URL || 'https://cloud1-2gfe5jhjef06b85d-1412172089.ap-shanghai.app.tcloudbase.com/mcpOAuth';
36
+
37
+ // Token 缓存
38
+ let cachedToken = null;
39
+ let tokenExpireTime = 0;
40
+
41
+ // OAuth 管理器实例
42
+ let oauthManager = null;
43
+
44
+ // 初始化 OAuth 管理器
45
+ function initOAuthManager() {
46
+ const tokenStorage = new FileTokenStorage(
47
+ path.join(os.homedir(), '.mcporter', 'oauth-token.json')
48
+ );
49
+
50
+ oauthManager = new OAuthManager({
51
+ mcpOAuthUrl: MCP_OAUTH_URL,
52
+ tokenStorage: tokenStorage,
53
+ onQrCode: (qrData) => {
54
+ },
55
+ onTokenReady: (token) => {
56
+ }
57
+ });
58
+
59
+ return oauthManager;
60
+ }
29
61
 
30
62
  // 创建 MCP Server
31
63
  const server = new Server(
32
64
  {
33
- name: 'chaihuo-mcp-server',
65
+ name: 'chaimi-bookkeeping-mcp',
34
66
  version: MCP_VERSION,
35
67
  },
36
68
  {
@@ -46,17 +78,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
46
78
  tools: [
47
79
  {
48
80
  name: 'save_expense',
49
- description: '保存单商品消费记录(AI文字记账)',
81
+ description: '保存单商品消费记录(AI文字记账)。只需提供商品名称和金额,其他参数自动填充。示例:name="午餐", amount=35',
50
82
  inputSchema: {
51
83
  type: 'object',
52
84
  properties: {
53
- name: { type: 'string', description: '商品名称' },
54
- amount: { type: 'number', description: '金额' },
55
- category: { type: 'string', description: '分类(如:餐饮、食品、交通)' },
56
- store: { type: 'string', description: '商家名称' },
85
+ name: { type: 'string', description: '商品名称(必填)' },
86
+ amount: { type: 'number', description: '金额(必填)' },
87
+ category: { type: 'string', description: '分类(可选,如:餐饮、食品、交通)' },
88
+ store: { type: 'string', description: '商家名称(可选)' },
89
+ note: { type: 'string', description: '备注(可选)' },
57
90
  agentType: { type: 'string', description: '【推荐自动填充】Agent类型,如:claude-desktop、cursor、openclaw、workbuddy、trae' },
58
91
  apiProvider: { type: 'string', description: '【推荐自动填充】AI服务提供商,如:anthropic、openai、doubao、aliyun' },
59
92
  rawInput: { type: 'string', description: '【推荐自动填充】用户的原始输入内容,用于记录用户原始请求' },
93
+ mcp_version: { type: 'string', description: 'MCP Server 版本号(自动填充)' },
60
94
  },
61
95
  required: ['name', 'amount'],
62
96
  },
@@ -75,28 +109,29 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
75
109
  actualAmount: { type: 'number', description: '实付金额' },
76
110
  paymentMethod: { type: 'string', description: '支付方式' },
77
111
  receiptNo: { type: 'string', description: '小票编号' },
78
- agentType: { type: 'string', description: '【推荐自动填充】Agent类型,如:claude-desktop、cursor、openclaw、workbuddy、trae' },
79
- apiProvider: { type: 'string', description: '【推荐自动填充】AI服务提供商,如:anthropic、openai、doubao、aliyun' },
80
- rawInput: { type: 'string', description: '【推荐自动填充】用户的原始输入内容,用于记录用户原始请求' },
81
112
  items: {
82
113
  type: 'array',
83
- description: '商品列表',
114
+ description: '商品列表(必须是数组格式,如:[{"name":"苹果","amount":5.5,"price":5.5,"quantity":1}],不要传JSON字符串)',
84
115
  items: {
85
116
  type: 'object',
86
117
  properties: {
87
118
  name: { type: 'string', description: '商品名称' },
88
119
  originalName: { type: 'string', description: '原始商品名称' },
89
120
  price: { type: 'number', description: '单价' },
90
- quantity: { type: 'string', description: '数量' },
121
+ quantity: { type: 'number', description: '数量' },
91
122
  unit: { type: 'string', description: '单位' },
92
123
  amount: { type: 'number', description: '金额' },
93
124
  weight: { type: 'string', description: '重量' },
94
125
  marketPrice: { type: 'string', description: '市场单价' },
95
126
  category: { type: 'string', description: '分类' },
96
127
  },
97
- required: ['name', 'amount'],
128
+ required: ['name', 'amount', 'price', 'quantity'],
98
129
  },
99
130
  },
131
+ agentType: { type: 'string', description: '【推荐自动填充】Agent类型,如:claude-desktop、cursor、openclaw、workbuddy、trae' },
132
+ apiProvider: { type: 'string', description: '【推荐自动填充】AI服务提供商,如:anthropic、openai、doubao、aliyun' },
133
+ rawInput: { type: 'string', description: '【推荐自动填充】用户的原始输入内容,用于记录用户原始请求' },
134
+ mcp_version: { type: 'string', description: 'MCP Server 版本号(自动填充)' },
100
135
  },
101
136
  required: ['store', 'items'],
102
137
  },
@@ -109,9 +144,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
109
144
  properties: {
110
145
  limit: { type: 'number', description: '返回数量限制', default: 10 },
111
146
  source: { type: 'string', description: '来源筛选' },
112
- agentType: { type: 'string', description: '【推荐自动填充】Agent类型,如:claude-desktop、cursor、openclaw、workbuddy、trae' },
113
- apiProvider: { type: 'string', description: '【推荐自动填充】AI服务提供商,如:anthropic、openai、doubao、aliyun' },
114
- rawInput: { type: 'string', description: '【推荐自动填充】用户的原始输入内容,用于记录用户原始请求' },
115
147
  },
116
148
  },
117
149
  },
@@ -122,9 +154,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
122
154
  type: 'object',
123
155
  properties: {
124
156
  limit: { type: 'number', description: '返回数量限制', default: 5 },
125
- agentType: { type: 'string', description: '【推荐自动填充】Agent类型,如:claude-desktop、cursor、openclaw、workbuddy、trae' },
126
- apiProvider: { type: 'string', description: '【推荐自动填充】AI服务提供商,如:anthropic、openai、doubao、aliyun' },
127
- rawInput: { type: 'string', description: '【推荐自动填充】用户的原始输入内容,用于记录用户原始请求' },
128
157
  },
129
158
  },
130
159
  },
@@ -136,72 +165,289 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
136
165
  properties: {
137
166
  yearMonth: { type: 'string', description: '年月(如:2026-03)' },
138
167
  type: { type: 'string', description: '统计类型:item/receipt/category' },
168
+ },
169
+ required: ['yearMonth'],
170
+ },
171
+ },
172
+ {
173
+ name: 'save_income',
174
+ description: '保存收入记录(工资、奖金、红包等)',
175
+ inputSchema: {
176
+ type: 'object',
177
+ properties: {
178
+ name: { type: 'string', description: '收入来源(如:工资、奖金、红包)' },
179
+ amount: { type: 'number', description: '收入金额' },
180
+ category: { type: 'string', description: '收入分类(如:工资、奖金、投资)' },
181
+ store: { type: 'string', description: '付款方(如:公司名称)' },
182
+ note: { type: 'string', description: '备注' },
139
183
  agentType: { type: 'string', description: '【推荐自动填充】Agent类型,如:claude-desktop、cursor、openclaw、workbuddy、trae' },
140
184
  apiProvider: { type: 'string', description: '【推荐自动填充】AI服务提供商,如:anthropic、openai、doubao、aliyun' },
141
185
  rawInput: { type: 'string', description: '【推荐自动填充】用户的原始输入内容,用于记录用户原始请求' },
186
+ mcp_version: { type: 'string', description: 'MCP Server 版本号(自动填充)' },
142
187
  },
143
- required: ['yearMonth'],
188
+ required: ['name', 'amount'],
144
189
  },
145
190
  },
146
191
  ],
147
192
  };
148
193
  });
149
194
 
195
+ // 工具名称映射:MCP Tool -> mcpHub Tool
196
+ const toolMapping = {
197
+ 'save_expense': 'addExpense',
198
+ 'save_receipt': 'addReceipt',
199
+ 'save_income': 'addIncome',
200
+ 'get_expenses': 'getExpenses',
201
+ 'get_receipt_list': 'getExpenses',
202
+ 'get_statistics': 'getStatistics',
203
+ };
204
+
205
+ // 获取或刷新 Token(OAuth 2.0 Device Flow)
206
+ const TOKEN_REFRESH_INTERVAL = 2 * 60 * 60 * 1000;
207
+ let lastRefreshTime = 0;
208
+
209
+ async function getToken() {
210
+ if (cachedToken && tokenExpireTime > Date.now() + 5 * 60 * 1000) {
211
+ if (Date.now() - lastRefreshTime < TOKEN_REFRESH_INTERVAL) {
212
+ return cachedToken;
213
+ }
214
+ }
215
+
216
+ if (!oauthManager) {
217
+ throw new Error('OAuth 管理器未初始化,请检查 MCP_OAUTH_URL 配置');
218
+ }
219
+
220
+ try {
221
+ const oauthToken = await oauthManager.getValidToken();
222
+ cachedToken = oauthToken.accessToken;
223
+ tokenExpireTime = oauthToken.expiresAt;
224
+ lastRefreshTime = Date.now();
225
+ await oauthManager.clearAuthState();
226
+ return cachedToken;
227
+ } catch (err) {
228
+ const authState = await oauthManager.loadAuthState();
229
+ if (authState && !oauthManager.isAuthStateExpired(authState)) {
230
+ throw new Error(`授权进行中,请使用验证码 ${authState.userCode} 完成授权`);
231
+ }
232
+
233
+ const newToken = await oauthManager.startAuthFlow();
234
+ cachedToken = newToken.accessToken;
235
+ tokenExpireTime = newToken.expiresAt;
236
+ lastRefreshTime = Date.now();
237
+ await oauthManager.clearAuthState();
238
+ return cachedToken;
239
+ }
240
+ }
241
+
242
+ // 调用 mcpPrompt 云函数(获取 Prompt 或校验数据)
243
+ async function callMcpPrompt(tool, params, token) {
244
+ const response = await fetch(MCP_PROMPT_URL, {
245
+ method: 'POST',
246
+ headers: {
247
+ 'Content-Type': 'application/json',
248
+ 'Authorization': `Bearer ${token}`,
249
+ },
250
+ body: JSON.stringify({
251
+ tool: tool,
252
+ params: params,
253
+ }),
254
+ });
255
+
256
+ if (!response.ok) {
257
+ const errorText = await response.text();
258
+ throw new Error(`mcpPrompt 调用失败: ${errorText}`);
259
+ }
260
+
261
+ return await response.json();
262
+ }
263
+
264
+ // 调用 mcpHub 云函数
265
+ async function callMcpHub(tool, params, token) {
266
+ const response = await fetch(MCP_HUB_URL, {
267
+ method: 'POST',
268
+ headers: {
269
+ 'Content-Type': 'application/json',
270
+ 'Authorization': `Bearer ${token}`,
271
+ },
272
+ body: JSON.stringify({
273
+ tool: tool,
274
+ params: params,
275
+ }),
276
+ });
277
+
278
+ if (!response.ok) {
279
+ const errorText = await response.text();
280
+ throw new Error(`mcpHub 调用失败: ${errorText}`);
281
+ }
282
+
283
+ return await response.json();
284
+ }
285
+
150
286
  // 工具调用处理
151
287
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
152
288
  const { name, arguments: args } = request.params;
153
289
 
154
- // 调试日志
155
- console.error('收到工具调用请求:', name);
156
- console.error('原始参数:', JSON.stringify(args, null, 2));
290
+ const agentTypeFromEnv = process.env.AGENT_TYPE || process.env.MCP_AGENT_TYPE || '';
291
+ const apiProviderFromEnv = process.env.API_PROVIDER || process.env.MCP_API_PROVIDER || '';
157
292
 
158
- try {
159
- // 获取 Token(这里需要实现 Token 生成逻辑)
160
- const token = await generateToken();
293
+ if (!args.agentType && agentTypeFromEnv) {
294
+ args.agentType = agentTypeFromEnv;
295
+ }
296
+ if (!args.apiProvider && apiProviderFromEnv) {
297
+ args.apiProvider = apiProviderFromEnv;
298
+ }
161
299
 
162
- // SCF 使用下划线命名,直接传递工具名
163
- const action = name;
300
+ try {
301
+ const token = await getToken();
164
302
 
165
- // 处理参数 - 将字符串格式的 items 转换为数组
166
303
  const processedArgs = { ...args };
167
304
  if (processedArgs.items && typeof processedArgs.items === 'string') {
168
305
  try {
169
306
  processedArgs.items = JSON.parse(processedArgs.items);
170
- console.error('items 解析成功:', processedArgs.items);
171
307
  } catch (e) {
172
- // 如果解析失败,保持原样
173
- console.error('解析 items 失败:', e);
174
308
  }
175
309
  }
176
310
 
177
- console.error('处理后参数:', JSON.stringify(processedArgs, null, 2));
311
+ let result;
312
+ let userMessage;
178
313
 
179
- // 调用 SCF
180
- const response = await fetch(SCF_URL, {
181
- method: 'POST',
182
- headers: {
183
- 'Content-Type': 'application/json',
184
- 'Authorization': `Bearer ${token}`,
185
- },
186
- body: JSON.stringify({
187
- action: action,
188
- data: processedArgs,
189
- }),
190
- });
191
-
192
- const result = await response.json();
193
-
194
- // 检查是否传递了agentType和apiProvider参数,如果没有则添加提示
195
- let responseText = JSON.stringify(result, null, 2);
196
- if (!processedArgs.agentType || !processedArgs.apiProvider) {
197
- responseText += '\n\n💡 提示:传递agentType和apiProvider参数可解锁小程序记录来源统计功能,示例:agentType="openclaw", apiProvider="doubao"';
314
+ switch (name) {
315
+ case 'save_expense': {
316
+ const validationResult = await callMcpPrompt(
317
+ 'validateResult',
318
+ {
319
+ data: processedArgs,
320
+ type: 'parseText'
321
+ },
322
+ token
323
+ );
324
+
325
+ if (validationResult.success && !validationResult.data.valid) {
326
+ const fillResult = await callMcpPrompt(
327
+ 'fillDefaults',
328
+ {
329
+ data: processedArgs,
330
+ type: 'parseText'
331
+ },
332
+ token
333
+ );
334
+
335
+ if (fillResult.success) {
336
+ processedArgs = fillResult.data;
337
+ }
338
+ }
339
+
340
+ const mcpParams = convertParams('save_expense', processedArgs);
341
+ result = await callMcpHub('addExpense', mcpParams, token);
342
+
343
+ if (result.success) {
344
+ const displayName = processedArgs.name || '未知商品';
345
+ const displayAmount = processedArgs.amount || 0;
346
+ const displayCategory = processedArgs.category || '其他';
347
+ const displayStore = processedArgs.store || '-';
348
+ userMessage = `✅ 记账成功\n\n| 商品 | 金额 | 分类 | 商家 |\n|------|------|------|------|\n| ${displayName} | ${displayAmount}元 | ${displayCategory} | ${displayStore} |\n\n已保存到你的柴米记账小程序数据库。`;
349
+
350
+ if (!processedArgs.agentType || !processedArgs.apiProvider) {
351
+ userMessage += '\n\n💡 提示:传递agentType和apiProvider参数可解锁小程序记录来源统计功能,示例:agentType="openclaw", apiProvider="doubao"';
352
+ }
353
+ }
354
+ break;
355
+ }
356
+
357
+ case 'save_receipt': {
358
+ const validationResult = await callMcpPrompt(
359
+ 'validateResult',
360
+ {
361
+ data: processedArgs,
362
+ type: 'parseReceipt'
363
+ },
364
+ token
365
+ );
366
+
367
+ if (validationResult.success && !validationResult.data.valid) {
368
+ const fillResult = await callMcpPrompt(
369
+ 'fillDefaults',
370
+ {
371
+ data: processedArgs,
372
+ type: 'parseReceipt'
373
+ },
374
+ token
375
+ );
376
+
377
+ if (fillResult.success) {
378
+ processedArgs = fillResult.data;
379
+ }
380
+ }
381
+
382
+ const mcpParams = convertParams('save_receipt', processedArgs);
383
+ result = await callMcpHub('addReceipt', mcpParams, token);
384
+
385
+ if (result.success) {
386
+ const totalAmount = processedArgs.totalAmount || processedArgs.items?.reduce((sum, item) => sum + (item.amount || 0), 0) || 0;
387
+ const itemsList = processedArgs.items?.map(item => `• ${item.name || '未知商品'} - ${item.amount || 0}元`).join('\n') || '暂无商品明细';
388
+ const storeName = processedArgs.store || '-';
389
+ const category = processedArgs.items?.[0]?.category || '其他';
390
+ userMessage = `✅ 小票记录成功\n\n| 项目 | 内容 |\n|------|------|\n| 商家 | ${storeName} |\n| 金额 | ${totalAmount}元 |\n| 分类 | ${category} |\n\n商品明细:\n${itemsList}\n\n已保存到你的柴米记账小程序数据库。`;
391
+
392
+ if (!processedArgs.agentType || !processedArgs.apiProvider) {
393
+ userMessage += '\n\n💡 提示:传递agentType和apiProvider参数可解锁小程序记录来源统计功能,示例:agentType="openclaw", apiProvider="doubao"';
394
+ }
395
+ }
396
+ break;
397
+ }
398
+
399
+ case 'get_expenses':
400
+ case 'get_receipt_list': {
401
+ const toolName = toolMapping[name];
402
+ const mcpParams = convertParams(name, processedArgs);
403
+ result = await callMcpHub(toolName, mcpParams, token);
404
+
405
+ if (result.success) {
406
+ userMessage = `📊 消费记录查询成功\n共找到 ${result.data?.length || 0} 条记录`;
407
+ }
408
+ break;
409
+ }
410
+
411
+ case 'get_statistics': {
412
+ const toolName = toolMapping[name];
413
+ const mcpParams = convertParams(name, processedArgs);
414
+ result = await callMcpHub(toolName, mcpParams, token);
415
+
416
+ if (result.success) {
417
+ userMessage = `📈 统计查询成功`;
418
+ }
419
+ break;
420
+ }
421
+
422
+ case 'save_income': {
423
+ const mcpParams = convertParams('save_income', processedArgs);
424
+ result = await callMcpHub('addIncome', mcpParams, token);
425
+
426
+ if (result.success) {
427
+ const displayName = processedArgs.name || '未知来源';
428
+ const displayAmount = processedArgs.amount || 0;
429
+ const displayCategory = processedArgs.category || '其他';
430
+ const displayStore = processedArgs.store || '-';
431
+ userMessage = `✅ 收入记录成功\n\n| 来源 | 金额 | 分类 | 付款方 |\n|------|------|------|--------|\n| ${displayName} | ${displayAmount}元 | ${displayCategory} | ${displayStore} |`;
432
+ }
433
+ break;
434
+ }
435
+
436
+ default:
437
+ throw new Error(`未知工具: ${name}`);
438
+ }
439
+
440
+ if (!result.success) {
441
+ userMessage = `❌ 操作失败:${result.error || '未知错误'}`;
198
442
  }
199
443
 
444
+ userMessage += `\n\n---\n📦 柴米记账 MCP v${MCP_VERSION}`;
445
+
200
446
  return {
201
447
  content: [
202
448
  {
203
449
  type: 'text',
204
- text: responseText,
450
+ text: userMessage,
205
451
  },
206
452
  ],
207
453
  };
@@ -218,44 +464,202 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
218
464
  }
219
465
  });
220
466
 
221
- // 生成 JWT Token
222
- async function generateToken() {
223
- const JWT_SECRET = process.env.JWT_SECRET;
224
- const MCP_OPENID = process.env.MCP_OPENID;
225
-
226
- if (!JWT_SECRET) {
227
- throw new Error('JWT_SECRET 环境变量未设置');
228
- }
467
+ function sanitizeString(str, maxLength = 200) {
468
+ if (!str || typeof str !== 'string') return '';
469
+ return str.replace(/<[^>]*>/g, '').substring(0, maxLength);
470
+ }
471
+
472
+ function validateAmount(amount) {
473
+ const num = parseFloat(amount);
474
+ return isNaN(num) || num < 0 ? 0 : num;
475
+ }
476
+
477
+ function validateQuantity(quantity) {
478
+ const num = parseFloat(quantity);
479
+ return isNaN(num) || num <= 0 ? 1 : num;
480
+ }
481
+
482
+ function extractWeightInGrams(weightStr) {
483
+ if (!weightStr || typeof weightStr !== 'string') return 0;
229
484
 
230
- if (!MCP_OPENID) {
231
- throw new Error('MCP_OPENID 环境变量未设置,请从小程序获取用户Token');
232
- }
485
+ const match = weightStr.match(/(\d+\.?\d*)\s*(g|kg|斤|克|千克)/i);
486
+ if (!match) return 0;
233
487
 
234
- // 使用 crypto 生成 JWT
235
- const crypto = require('crypto');
488
+ const value = parseFloat(match[1]);
489
+ const unit = match[2].toLowerCase();
236
490
 
237
- const header = { alg: 'HS256', typ: 'JWT' };
238
- const payload = {
239
- openid: MCP_OPENID, // 使用固定的用户 openid
240
- iat: Math.floor(Date.now() / 1000),
241
- exp: Math.floor(Date.now() / 1000) + 86400
242
- };
491
+ switch (unit) {
492
+ case 'g':
493
+ case '克':
494
+ return value;
495
+ case 'kg':
496
+ case '千克':
497
+ return value * 1000;
498
+ case '斤':
499
+ return value * 500;
500
+ default:
501
+ return value;
502
+ }
503
+ }
504
+
505
+ function calculateMarketPrice(amount, weightStr) {
506
+ const amountNum = parseFloat(amount);
507
+ const weightInGrams = extractWeightInGrams(weightStr);
243
508
 
244
- const h = Buffer.from(JSON.stringify(header)).toString('base64url');
245
- const p = Buffer.from(JSON.stringify(payload)).toString('base64url');
246
- const sig = crypto
247
- .createHmac('sha256', JWT_SECRET)
248
- .update(h + '.' + p)
249
- .digest('base64url');
509
+ if (isNaN(amountNum) || amountNum <= 0 || weightInGrams <= 0) {
510
+ return '';
511
+ }
250
512
 
251
- return `${h}.${p}.${sig}`;
513
+ const pricePer500g = (amountNum / weightInGrams) * 500;
514
+ return `${pricePer500g.toFixed(2)}元/500g`;
515
+ }
516
+
517
+ function convertParams(toolName, args) {
518
+ switch (toolName) {
519
+ case 'save_expense': {
520
+ if (!args.name || typeof args.name !== 'string' || args.name.trim() === '') {
521
+ throw new Error('缺少必填参数:name(商品名称)');
522
+ }
523
+ if (args.amount === undefined || args.amount === null) {
524
+ throw new Error('缺少必填参数:amount(金额)');
525
+ }
526
+
527
+ const name = sanitizeString(args.name, 100);
528
+ const amount = validateAmount(args.amount);
529
+
530
+ return {
531
+ name: name,
532
+ originalName: name,
533
+ amount: amount,
534
+ price: amount,
535
+ quantity: 1,
536
+ category: sanitizeString(args.category, 50) || '其他',
537
+ store: sanitizeString(args.store, 100) || '',
538
+ transactionType: 'expense',
539
+ unit: '',
540
+ weight: '',
541
+ marketPrice: '',
542
+ note: sanitizeString(args.note, 500) || '',
543
+ rawInput: sanitizeString(args.rawInput, 1000) || '',
544
+ agentType: args.agentType || '',
545
+ apiProvider: args.apiProvider || '',
546
+ mcp_version: args.mcp_version || MCP_VERSION,
547
+ source: 'mcp_txt_expense',
548
+ };
549
+ }
550
+
551
+ case 'save_receipt': {
552
+ let items = args.items;
553
+ if (typeof items === 'string') {
554
+ try {
555
+ items = JSON.parse(items);
556
+ } catch (e) {
557
+ throw new Error('items 参数格式错误:必须是数组或 JSON 数组字符串');
558
+ }
559
+ }
560
+ if (!Array.isArray(items)) {
561
+ throw new Error('items 参数必须是数组');
562
+ }
563
+
564
+ return {
565
+ items: items.map((item, index) => {
566
+ const weight = sanitizeString(item.weight, 50);
567
+ const amount = validateAmount(item.amount);
568
+ const marketPrice = sanitizeString(item.marketPrice, 50) || calculateMarketPrice(amount, weight);
569
+
570
+ return {
571
+ itemIndex: index,
572
+ name: sanitizeString(item.name, 100),
573
+ originalName: sanitizeString(item.originalName, 200) || sanitizeString(item.name, 100),
574
+ amount: amount,
575
+ category: sanitizeString(item.category, 50) || '其他',
576
+ subCategory: sanitizeString(item.subCategory, 50) || '',
577
+ transactionType: item.transactionType || 'expense',
578
+ price: validateAmount(item.price),
579
+ quantity: item.quantity ? String(item.quantity).substring(0, 20) : '1',
580
+ unit: sanitizeString(item.unit, 20),
581
+ weight: weight,
582
+ marketPrice: marketPrice,
583
+ note: sanitizeString(item.note, 500),
584
+ };
585
+ }),
586
+ store: sanitizeString(args.store, 100),
587
+ receiptNo: sanitizeString(args.receiptNo, 50),
588
+ totalAmount: validateAmount(args.totalAmount),
589
+ originalAmount: validateAmount(args.originalAmount),
590
+ discountAmount: validateAmount(args.discountAmount),
591
+ actualAmount: validateAmount(args.actualAmount),
592
+ paymentMethod: sanitizeString(args.paymentMethod, 50),
593
+ memberCardNo: sanitizeString(args.memberCardNo, 50),
594
+ currentPoints: parseInt(args.currentPoints) || 0,
595
+ totalPoints: parseInt(args.totalPoints) || 0,
596
+ rawInput: sanitizeString(args.rawInput, 2000),
597
+ agentType: args.agentType || '',
598
+ apiProvider: args.apiProvider || '',
599
+ mcp_version: args.mcp_version || MCP_VERSION,
600
+ source: 'mcp_receipt',
601
+ };
602
+ }
603
+
604
+ case 'get_expenses':
605
+ return {
606
+ limit: Math.min(parseInt(args.limit) || 10, 100),
607
+ source: args.source,
608
+ };
609
+
610
+ case 'get_statistics':
611
+ return {
612
+ startDate: args.yearMonth ? `${args.yearMonth}-01` : undefined,
613
+ endDate: args.yearMonth ? getMonthEndDate(args.yearMonth) : undefined,
614
+ type: args.type,
615
+ };
616
+
617
+ case 'save_income':
618
+ return {
619
+ name: sanitizeString(args.name, 100),
620
+ amount: validateAmount(args.amount),
621
+ category: sanitizeString(args.category, 50) || '其他',
622
+ store: sanitizeString(args.store, 100),
623
+ note: sanitizeString(args.note, 500),
624
+ transactionType: 'income',
625
+ rawInput: sanitizeString(args.rawInput, 1000),
626
+ agentType: args.agentType || '',
627
+ apiProvider: args.apiProvider || '',
628
+ mcp_version: args.mcp_version || MCP_VERSION,
629
+ source: 'mcp_txt_income',
630
+ };
631
+
632
+ default:
633
+ return args;
634
+ }
635
+ }
636
+
637
+ function getMonthEndDate(yearMonth) {
638
+ const [year, month] = yearMonth.split('-').map(Number);
639
+ const lastDay = new Date(year, month, 0).getDate();
640
+ return `${yearMonth}-${lastDay}`;
252
641
  }
253
642
 
254
- // 启动服务器
255
643
  async function main() {
644
+ initOAuthManager();
645
+
646
+ try {
647
+ const existingToken = await oauthManager.tokenStorage.load();
648
+ if (existingToken) {
649
+ const validToken = await oauthManager.getValidToken();
650
+ cachedToken = validToken.accessToken;
651
+ tokenExpireTime = validToken.expiresAt;
652
+ } else {
653
+ const newToken = await oauthManager.startAuthFlow();
654
+ cachedToken = newToken.accessToken;
655
+ tokenExpireTime = newToken.expiresAt;
656
+ }
657
+ } catch (err) {
658
+ throw err;
659
+ }
660
+
256
661
  const transport = new StdioServerTransport();
257
662
  await server.connect(transport);
258
- console.error('柴米记账 MCP Server 已启动');
259
663
  }
260
664
 
261
- main().catch(console.error);
665
+ main().catch(() => {});
package/test-mcp.sh ADDED
@@ -0,0 +1,26 @@
1
+ #!/bin/bash
2
+
3
+ # MCP Server 本地测试脚本
4
+
5
+ echo "=== 柴米记账 MCP Server 本地测试 ==="
6
+ echo ""
7
+
8
+ # 设置环境变量(测试环境)
9
+ export MCP_HUB_URL="https://chaihuo-xxx.ap-shanghai.app.tcloudbase.com/mcpHub-mcp"
10
+ export MCP_PROMPT_URL="https://chaihuo-xxx.ap-shanghai.app.tcloudbase.com/mcpPrompt"
11
+ export MCP_OAUTH_URL="https://chaihuo-xxx.ap-shanghai.app.tcloudbase.com/mcpOAuth"
12
+ export AGENT_TYPE="test"
13
+ export API_PROVIDER="test"
14
+
15
+ echo "环境变量设置:"
16
+ echo " MCP_HUB_URL: $MCP_HUB_URL"
17
+ echo " AGENT_TYPE: $AGENT_TYPE"
18
+ echo " API_PROVIDER: $API_PROVIDER"
19
+ echo ""
20
+
21
+ # 测试 MCP Server 启动
22
+ echo "=== 测试 MCP Server 启动 ==="
23
+ echo "按 Ctrl+C 停止测试"
24
+ echo ""
25
+
26
+ node server.js