server.js 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788
  1. #!/usr/bin/env node
  2. const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
  3. const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
  4. const { z } = require('zod');
  5. const { readXiaohongshuToken, readVocToken } = require('./core/credentials');
  6. const { runXiaohongshuTrend } = require('./tools/xiaohongshu-trend-run');
  7. const { updateXiaohongshuPreference } = require('./tools/xiaohongshu-preference-update');
  8. const { runDouyinTrend } = require('./tools/douyin-trend-run');
  9. const { updateDouyinPreference } = require('./tools/douyin-preference-update');
  10. const { runVocProblemDeepDive } = require('./tools/voc-problem-deep-dive-run');
  11. const { runVocIssuePool } = require('./tools/voc-issue-pool-run');
  12. const { runVocContentPlan } = require('./tools/voc-content-plan-run');
  13. const { runVocCompetitorMap } = require('./tools/voc-competitor-map-run');
  14. const { runVocSpeakingScript } = require('./tools/voc-speaking-script-run');
  15. const { runBusinessWorkflow } = require('./tools/voc-business-workflow-run');
  16. const { analyzeFmodeImage } = require('./tools/fmode-image-analysis');
  17. const { searchVocApis, getVocApiDoc, callVocApi } = require('./tools/voc-api-catalog-run');
  18. const { buildVocRechargeInfo, buildMissingTokenMessage } = require('./core/payment-links');
  19. function asToolResult(result) {
  20. const normalized = normalizeToolResult(result);
  21. return {
  22. content: [
  23. {
  24. type: 'text',
  25. text: normalized.assistantMessage || JSON.stringify(normalized, null, 2)
  26. }
  27. ],
  28. structuredContent: normalized,
  29. isError: normalized.status !== 'ok'
  30. };
  31. }
  32. function normalizeWarning(value) {
  33. if (typeof value === 'string') return value;
  34. if (!value || typeof value !== 'object') return String(value || '');
  35. const parts = [
  36. value.stage,
  37. value.keyword && `keyword=${value.keyword}`,
  38. value.noteId && `noteId=${value.noteId}`,
  39. value.videoId && `videoId=${value.videoId}`,
  40. value.httpStatus && `http=${value.httpStatus}`,
  41. value.message
  42. ].filter(Boolean);
  43. return parts.join(' | ') || JSON.stringify(value);
  44. }
  45. function normalizeToolResult(result = {}) {
  46. const normalized = { ...result };
  47. if (Object.prototype.hasOwnProperty.call(result, 'warnings')) {
  48. normalized.warnings = Array.isArray(result.warnings)
  49. ? result.warnings.map(normalizeWarning).filter(Boolean)
  50. : [];
  51. }
  52. if (Object.prototype.hasOwnProperty.call(result, 'errors')) {
  53. normalized.errors = Array.isArray(result.errors) ? result.errors : [];
  54. }
  55. return normalized;
  56. }
  57. function createServer() {
  58. const server = new McpServer({
  59. name: 'voc-intelligence',
  60. version: '0.3.17'
  61. });
  62. server.registerTool(
  63. 'fmode_image_analysis',
  64. {
  65. title: 'Analyze Images With Fmode Doubao Vision',
  66. description: [
  67. 'Analyze screenshots, product images, UI images, charts, posters, or local image files through Fmode OpenAI-compatible Doubao vision.',
  68. 'Use when the current Claude Code turn includes uploaded or pasted image attachments surfaced as local temp file paths, file objects, or data URLs.',
  69. 'Use when the user asks for 图片识别, 识图, 看图, 分析截图, 提取图片文字, UI 截图分析, or when a text-only model such as DeepSeek cannot read an image.',
  70. 'Default model is doubao-seed-2-0-pro-260215 and billing uses the configured platform user token instead of a separate model key.'
  71. ].join(' '),
  72. inputSchema: {
  73. imagePath: z.string().optional(),
  74. imagePaths: z.array(z.string()).optional(),
  75. imageUrl: z.string().optional(),
  76. imageUrls: z.array(z.string()).optional(),
  77. image: z.union([z.string(), z.object({}).passthrough()]).optional(),
  78. images: z.array(z.union([z.string(), z.object({}).passthrough()])).optional(),
  79. file: z.union([z.string(), z.object({}).passthrough()]).optional(),
  80. files: z.array(z.union([z.string(), z.object({}).passthrough()])).optional(),
  81. attachment: z.union([z.string(), z.object({}).passthrough()]).optional(),
  82. attachments: z.array(z.union([z.string(), z.object({}).passthrough()])).optional(),
  83. prompt: z.string().optional(),
  84. question: z.string().optional(),
  85. text: z.string().optional(),
  86. context: z.string().optional(),
  87. outputFormat: z.enum(['text', 'json']).optional(),
  88. responseFormat: z.enum(['text', 'json']).optional(),
  89. detail: z.enum(['auto', 'low', 'high']).optional(),
  90. temperature: z.number().optional(),
  91. maxTokens: z.number().int().min(64).max(8192).optional(),
  92. maxImageBytes: z.number().int().min(1024).optional(),
  93. output: z.string().optional(),
  94. outputPath: z.string().optional(),
  95. outputDir: z.string().optional(),
  96. baseUrl: z.string().optional(),
  97. path: z.string().optional(),
  98. fmodeBaseUrl: z.string().optional(),
  99. fmodePath: z.string().optional(),
  100. fmodeModel: z.string().optional(),
  101. model: z.string().optional(),
  102. doubaoVisionBaseUrl: z.string().optional(),
  103. doubaoVisionPath: z.string().optional(),
  104. doubaoVisionModel: z.string().optional(),
  105. videoAnalysisBaseUrl: z.string().optional(),
  106. videoAnalysisPath: z.string().optional(),
  107. videoAnalysisModel: z.string().optional(),
  108. fmodeToken: z.string().optional(),
  109. imageAnalysisToken: z.string().optional(),
  110. doubaoVisionToken: z.string().optional(),
  111. videoAnalysisToken: z.string().optional(),
  112. xiaohongshuToken: z.string().optional(),
  113. douyinToken: z.string().optional(),
  114. vocToken: z.string().optional(),
  115. tihaoToken: z.string().optional(),
  116. sessionToken: z.string().optional(),
  117. token: z.string().optional(),
  118. apiToken: z.string().optional(),
  119. allowEnvToken: z.boolean().optional()
  120. },
  121. outputSchema: {
  122. status: z.string(),
  123. assistantMessage: z.string(),
  124. summary: z.object({}).passthrough().optional(),
  125. data: z.object({}).passthrough().optional(),
  126. files: z.array(z.string()).optional(),
  127. nextActions: z.array(z.string()).optional(),
  128. warnings: z.array(z.any()).optional(),
  129. errors: z.array(z.any()).optional()
  130. }
  131. },
  132. async input => asToolResult(await analyzeFmodeImage(input))
  133. );
  134. server.registerTool(
  135. 'voc_business_workflow_run',
  136. {
  137. title: 'Run VOC Business Workflow',
  138. description: [
  139. 'Run the full VOC business workflow from market voices to issue pool, top issue deep dive, 7-day content plan, and one speaking script.',
  140. 'Use when the user asks to see what customers care about, decide what to fix first, and know what to post next week in one end-to-end workflow.'
  141. ].join(' '),
  142. inputSchema: {
  143. project: z.string().optional(),
  144. brand: z.string().optional(),
  145. store: z.string().optional(),
  146. industry: z.string().optional(),
  147. category: z.string().optional(),
  148. platform: z.enum(['douyin', 'xiaohongshu', '\u6296\u97f3', '\u5c0f\u7ea2\u4e66']).optional(),
  149. collectionMode: z.enum(['sample', 'live']).optional(),
  150. scenario: z.string().optional(),
  151. scene: z.string().optional(),
  152. audience: z.string().optional(),
  153. targetAudience: z.string().optional(),
  154. keywords: z.array(z.string()).optional(),
  155. keyword: z.string().optional(),
  156. issue: z.string().optional(),
  157. problem: z.string().optional(),
  158. topic: z.string().optional(),
  159. feedback: z.string().optional(),
  160. finalize: z.boolean().optional(),
  161. output: z.string().optional(),
  162. keywordLimit: z.number().int().min(1).max(10).optional(),
  163. notesPerKeyword: z.number().int().min(1).max(10).optional(),
  164. videosPerKeyword: z.number().int().min(1).max(10).optional(),
  165. maxCommentPages: z.number().int().min(0).max(5).optional(),
  166. xiaohongshuToken: z.string().optional(),
  167. douyinToken: z.string().optional(),
  168. vocToken: z.string().optional()
  169. },
  170. outputSchema: {
  171. status: z.string(),
  172. assistantMessage: z.string(),
  173. summary: z.object({}).passthrough().optional(),
  174. data: z.object({}).passthrough().optional(),
  175. files: z.array(z.string()).optional(),
  176. nextActions: z.array(z.string()).optional(),
  177. warnings: z.array(z.any()).optional(),
  178. errors: z.array(z.any()).optional()
  179. }
  180. },
  181. async input => asToolResult(await runBusinessWorkflow(input))
  182. );
  183. server.registerTool(
  184. 'voc_speaking_script_run',
  185. {
  186. title: 'Run VOC Speaking Script Co-Creation',
  187. description: [
  188. 'Turn one VOC-backed topic into a co-created speaking script.',
  189. 'Use after a content plan when the user selects a topic, asks to enter script co-creation, revise a speaking script, finalize a draft, or save script preferences.'
  190. ].join(' '),
  191. inputSchema: {
  192. project: z.string().optional(),
  193. brand: z.string().optional(),
  194. store: z.string().optional(),
  195. industry: z.string().optional(),
  196. category: z.string().optional(),
  197. topic: z.string().optional(),
  198. title: z.string().optional(),
  199. selectedTopic: z.string().optional(),
  200. userIssue: z.string().optional(),
  201. issue: z.string().optional(),
  202. problem: z.string().optional(),
  203. issues: z.array(z.string()).optional(),
  204. problems: z.array(z.string()).optional(),
  205. vocIssues: z.array(z.string()).optional(),
  206. evidence: z.array(z.string()).optional(),
  207. comments: z.array(z.string()).optional(),
  208. voices: z.array(z.string()).optional(),
  209. vocEvidence: z.array(z.string()).optional(),
  210. evidenceText: z.string().optional(),
  211. feedback: z.string().optional(),
  212. message: z.string().optional(),
  213. preferredStyles: z.array(z.string()).optional(),
  214. blockedStyles: z.array(z.string()).optional(),
  215. finalize: z.boolean().optional(),
  216. memory: z.string().optional(),
  217. memoryPath: z.string().optional(),
  218. output: z.string().optional()
  219. },
  220. outputSchema: {
  221. status: z.string(),
  222. assistantMessage: z.string(),
  223. summary: z.object({}).passthrough().optional(),
  224. data: z.object({}).passthrough().optional(),
  225. files: z.array(z.string()).optional(),
  226. nextActions: z.array(z.string()).optional(),
  227. warnings: z.array(z.any()).optional(),
  228. errors: z.array(z.any()).optional()
  229. }
  230. },
  231. async input => asToolResult(await runVocSpeakingScript(input))
  232. );
  233. server.registerTool(
  234. 'voc_content_plan_run',
  235. {
  236. title: 'Run VOC Content Plan',
  237. description: [
  238. 'Turn VOC issues, comments, trend reports, or issue-pool findings into a 7-day content plan and speaking scripts.',
  239. 'Use when the user asks what to post next week, wants口播脚本, video topics, account content ideas, or wants to turn customer problems into marketing content.'
  240. ].join(' '),
  241. inputSchema: {
  242. project: z.string().optional(),
  243. brand: z.string().optional(),
  244. store: z.string().optional(),
  245. industry: z.string().optional(),
  246. category: z.string().optional(),
  247. platform: z.string().optional(),
  248. days: z.number().int().min(1).max(15).optional(),
  249. issues: z.array(z.string()).optional(),
  250. problems: z.array(z.string()).optional(),
  251. vocIssues: z.array(z.string()).optional(),
  252. contentDirections: z.array(z.string()).optional(),
  253. directions: z.array(z.string()).optional(),
  254. evidence: z.array(z.string()).optional(),
  255. comments: z.array(z.string()).optional(),
  256. evidenceText: z.string().optional(),
  257. reportText: z.string().optional(),
  258. context: z.string().optional(),
  259. background: z.string().optional(),
  260. businessGoal: z.string().optional(),
  261. priceBand: z.string().optional(),
  262. targetAudience: z.string().optional(),
  263. report: z.string().optional(),
  264. reportPath: z.string().optional()
  265. },
  266. outputSchema: {
  267. status: z.string(),
  268. assistantMessage: z.string(),
  269. summary: z.object({}).passthrough().optional(),
  270. data: z.object({}).passthrough().optional(),
  271. files: z.array(z.string()).optional(),
  272. nextActions: z.array(z.string()).optional(),
  273. warnings: z.array(z.any()).optional(),
  274. errors: z.array(z.any()).optional()
  275. }
  276. },
  277. async input => asToolResult(await runVocContentPlan(input))
  278. );
  279. server.registerTool(
  280. 'voc_competitor_map_run',
  281. {
  282. title: 'Run VOC Competitor Map',
  283. description: [
  284. 'Build a competitor map and differentiated opportunity report from category, city, competitors, and VOC evidence.',
  285. 'Use when the user asks to look at competitors, competitor analysis, who else is doing well, or how the brand should compete differently.'
  286. ].join(' '),
  287. inputSchema: {
  288. project: z.string().optional(),
  289. brand: z.string().optional(),
  290. store: z.string().optional(),
  291. industry: z.string().optional(),
  292. category: z.string().optional(),
  293. city: z.string().optional(),
  294. region: z.string().optional(),
  295. priceBand: z.string().optional(),
  296. price: z.string().optional(),
  297. scenario: z.string().optional(),
  298. scene: z.string().optional(),
  299. competitors: z.array(z.string()).optional(),
  300. competitorNames: z.array(z.string()).optional(),
  301. evidence: z.array(z.string()).optional(),
  302. comments: z.array(z.string()).optional(),
  303. voices: z.array(z.string()).optional(),
  304. reportText: z.string().optional(),
  305. report: z.string().optional(),
  306. reportPath: z.string().optional()
  307. },
  308. outputSchema: {
  309. status: z.string(),
  310. assistantMessage: z.string(),
  311. summary: z.object({}).passthrough().optional(),
  312. data: z.object({}).passthrough().optional(),
  313. files: z.array(z.string()).optional(),
  314. nextActions: z.array(z.string()).optional(),
  315. warnings: z.array(z.any()).optional(),
  316. errors: z.array(z.any()).optional()
  317. }
  318. },
  319. async input => asToolResult(await runVocCompetitorMap(input))
  320. );
  321. server.registerTool(
  322. 'voc_issue_pool_run',
  323. {
  324. title: 'Run VOC Issue Pool',
  325. description: [
  326. 'Turn user comments, report text, or VOC snippets into a prioritized issue pool.',
  327. 'Use when the user asks which real problems matter most, asks to manage VOC issues, or wants a problem list before deep-diving.'
  328. ].join(' '),
  329. inputSchema: {
  330. project: z.string().optional().describe('Optional project, brand, account, or store name used to isolate issue-pool memory.'),
  331. brand: z.string().optional(),
  332. store: z.string().optional(),
  333. industry: z.string().optional(),
  334. category: z.string().optional(),
  335. scenario: z.string().optional(),
  336. scene: z.string().optional(),
  337. businessType: z.string().optional(),
  338. audience: z.string().optional(),
  339. targetAudience: z.string().optional(),
  340. evidence: z.array(z.string()).optional().describe('User comments or VOC snippets.'),
  341. comments: z.array(z.string()).optional().describe('User comment snippets.'),
  342. voices: z.array(z.string()).optional().describe('User voice snippets.'),
  343. issues: z.array(z.string()).optional().describe('Known issue snippets.'),
  344. issueTexts: z.array(z.string()).optional().describe('Known issue snippets.'),
  345. evidenceText: z.string().optional().describe('Raw comments or evidence text.'),
  346. reportText: z.string().optional().describe('Previous report markdown text.'),
  347. report: z.string().optional().describe('Optional previous report markdown path.'),
  348. reportPath: z.string().optional().describe('Optional previous report markdown path.'),
  349. memory: z.string().optional().describe('Optional issue-pool memory JSON path.'),
  350. memoryPath: z.string().optional().describe('Optional issue-pool memory JSON path.'),
  351. feedback: z.string().optional().describe('User feedback or status notes.'),
  352. message: z.string().optional().describe('User feedback or status notes.'),
  353. resolvedIssues: z.array(z.string()).optional().describe('Issue names or ids already resolved.'),
  354. validatingIssues: z.array(z.string()).optional().describe('Issue names or ids currently validating.'),
  355. blockedIssues: z.array(z.string()).optional().describe('Issue names or ids to pause.'),
  356. statusUpdates: z.record(z.string()).optional().describe('Map of issue title/id to status.')
  357. },
  358. outputSchema: {
  359. status: z.string(),
  360. assistantMessage: z.string(),
  361. summary: z.object({}).passthrough().optional(),
  362. data: z.object({}).passthrough().optional(),
  363. files: z.array(z.string()).optional(),
  364. nextActions: z.array(z.string()).optional(),
  365. warnings: z.array(z.any()).optional(),
  366. errors: z.array(z.any()).optional()
  367. }
  368. },
  369. async input => {
  370. const result = await runVocIssuePool(input);
  371. return asToolResult(result);
  372. }
  373. );
  374. server.registerTool(
  375. 'voc_problem_deep_dive_run',
  376. {
  377. title: 'Run VOC Problem Deep Dive',
  378. description: [
  379. 'Deep-dive a single VOC problem from a boss/operator perspective.',
  380. 'Use when the user says continue digging, what should the boss do, how to solve this problem, or turn this VOC into actions.'
  381. ].join(' '),
  382. inputSchema: {
  383. issue: z.string().optional().describe('Single VOC issue to deep dive, e.g. 排队, 贵, 不好吃, 服务差.'),
  384. problem: z.string().optional().describe('Alias for issue.'),
  385. topic: z.string().optional().describe('Alias for issue.'),
  386. project: z.string().optional().describe('Optional project, brand, or account name used to isolate deep-dive memory.'),
  387. brand: z.string().optional().describe('Optional brand name used to isolate deep-dive memory.'),
  388. store: z.string().optional().describe('Optional store name used to isolate deep-dive memory.'),
  389. industry: z.string().optional(),
  390. category: z.string().optional(),
  391. scenario: z.string().optional(),
  392. scene: z.string().optional(),
  393. businessType: z.string().optional(),
  394. audience: z.string().optional(),
  395. targetAudience: z.string().optional(),
  396. evidence: z.array(z.string()).optional().describe('Optional user comments or VOC snippets.'),
  397. evidenceText: z.string().optional().describe('Optional text with comments or evidence.'),
  398. comments: z.array(z.string()).optional().describe('Optional comment snippets.'),
  399. voices: z.array(z.string()).optional().describe('Optional user voice snippets.'),
  400. report: z.string().optional().describe('Optional previous report markdown path.'),
  401. reportPath: z.string().optional().describe('Optional previous report markdown path.'),
  402. memory: z.string().optional().describe('Optional deep-dive memory JSON path.'),
  403. memoryPath: z.string().optional().describe('Optional deep-dive memory JSON path.'),
  404. feedback: z.string().optional().describe('User feedback from previous iteration.'),
  405. message: z.string().optional().describe('User feedback from previous iteration.'),
  406. preferredActions: z.array(z.string()).optional().describe('Actions the user prefers.'),
  407. blockedActions: z.array(z.string()).optional().describe('Actions the user does not want.'),
  408. preferredContentAngles: z.array(z.string()).optional().describe('Content angles the user prefers.'),
  409. validatedActions: z.array(z.string()).optional().describe('Actions proven useful in practice.'),
  410. rejectedActions: z.array(z.string()).optional().describe('Actions proven ineffective in practice.')
  411. },
  412. outputSchema: {
  413. status: z.string(),
  414. assistantMessage: z.string(),
  415. summary: z.object({}).passthrough().optional(),
  416. data: z.object({}).passthrough().optional(),
  417. files: z.array(z.string()).optional(),
  418. nextActions: z.array(z.string()).optional(),
  419. warnings: z.array(z.any()).optional(),
  420. errors: z.array(z.any()).optional()
  421. }
  422. },
  423. async input => {
  424. const result = await runVocProblemDeepDive(input);
  425. return asToolResult(result);
  426. }
  427. );
  428. server.registerTool(
  429. 'voc_xiaohongshu_token_check',
  430. {
  431. title: 'Check Xiaohongshu Token',
  432. description: 'Check whether a Xiaohongshu/TikHub API token is configured for live collection.',
  433. inputSchema: {
  434. xiaohongshuToken: z.string().optional().describe('Optional request-scoped collection token. It is never echoed back.'),
  435. vocToken: z.string().optional().describe('Optional VOC social token, same convention as douyin-speaking-daily. It is never echoed back.')
  436. },
  437. outputSchema: {
  438. status: z.string(),
  439. assistantMessage: z.string(),
  440. configured: z.boolean(),
  441. data: z.object({}).passthrough().optional()
  442. }
  443. },
  444. async input => {
  445. const token = readXiaohongshuToken(input);
  446. const recharge = token ? null : await buildVocRechargeInfo();
  447. const result = {
  448. status: token ? 'ok' : 'needs_token',
  449. configured: Boolean(token),
  450. data: recharge ? { recharge } : {},
  451. assistantMessage: token
  452. ? '\u5c0f\u7ea2\u4e66 live \u91c7\u96c6 Token \u5df2\u914d\u7f6e\uff0c\u53ef\u4ee5\u5c1d\u8bd5\u5c0f\u89c4\u6a21\u771f\u5b9e\u91c7\u96c6\u3002'
  453. : buildMissingTokenMessage(recharge.paymentUrl)
  454. };
  455. return asToolResult(result);
  456. }
  457. );
  458. server.registerTool(
  459. 'voc_douyin_token_check',
  460. {
  461. title: 'Check Douyin Token',
  462. description: 'Check whether a Douyin/VOC API token is configured for live collection.',
  463. inputSchema: {
  464. douyinToken: z.string().optional().describe('Optional request-scoped Douyin token. It is never echoed back.'),
  465. vocToken: z.string().optional().describe('Optional VOC social token, same convention as xiaohongshu trend.'),
  466. xiaohongshuToken: z.string().optional().describe('Optional platform token alias, never echoed back.')
  467. },
  468. outputSchema: {
  469. status: z.string(),
  470. assistantMessage: z.string(),
  471. configured: z.boolean(),
  472. data: z.object({}).passthrough().optional()
  473. }
  474. },
  475. async input => {
  476. const token = readVocToken(input);
  477. const recharge = token ? null : await buildVocRechargeInfo();
  478. const result = {
  479. status: token ? 'ok' : 'needs_token',
  480. configured: Boolean(token),
  481. data: recharge ? { recharge } : {},
  482. assistantMessage: token
  483. ? '\u6296\u97f3 live \u91c7\u96c6 Token \u5df2\u914d\u7f6e\uff0c\u53ef\u4ee5\u5f00\u59cb\u5c0f\u89c4\u6a21\u771f\u5b9e\u91c7\u96c6\u3002'
  484. : buildMissingTokenMessage(recharge.paymentUrl, recharge, { platformLabel: '\u6296\u97f3' })
  485. };
  486. return asToolResult(result);
  487. }
  488. );
  489. server.registerTool(
  490. 'voc_xiaohongshu_trend_run',
  491. {
  492. title: 'Run Xiaohongshu Trend Intelligence',
  493. description: [
  494. 'Generate first-round Xiaohongshu trend observations and validation questions from an industry profile.',
  495. 'P0 supports sample mode for demos and course recording.',
  496. 'Returns assistantMessage as the user-facing body; first-round output is a hypothesis that needs user calibration.'
  497. ].join(' '),
  498. inputSchema: {
  499. collectionMode: z.enum(['sample', 'live']).optional().describe('Use sample for P0 demo. Live will be enabled after VOC provider integration.'),
  500. profile: z.string().optional().describe('Optional profile JSON path.'),
  501. output: z.string().optional().describe('Optional output directory.'),
  502. memory: z.string().optional().describe('Optional preference memory JSON path.'),
  503. memoryPath: z.string().optional().describe('Optional preference memory JSON path.'),
  504. project: z.string().optional(),
  505. industry: z.string().optional(),
  506. businessType: z.string().optional(),
  507. targetAudience: z.array(z.string()).optional(),
  508. keywords: z.array(z.string()).optional(),
  509. trendQuestions: z.array(z.string()).optional(),
  510. mustTrackSignals: z.array(z.string()).optional(),
  511. keywordLimit: z.number().int().min(1).max(10).optional(),
  512. notesPerKeyword: z.number().int().min(1).max(10).optional(),
  513. maxCommentPages: z.number().int().min(0).max(5).optional(),
  514. sort: z.enum(['general', 'time_descending', 'popularity_descending']).optional(),
  515. noteType: z.enum(['_0', '_1', '_2']).optional(),
  516. cacheAssets: z.boolean().optional().describe('Whether to cache evidence images into the output assets directory. Defaults to true.'),
  517. assetLimit: z.number().int().min(0).max(30).optional().describe('Maximum number of note cover images to cache.'),
  518. xiaohongshuToken: z.string().optional().describe('Optional request-scoped collection token. It is used only for this run and never echoed back.'),
  519. vocToken: z.string().optional().describe('Optional VOC social token, same convention as douyin-speaking-daily. It is used only for this run and never echoed back.')
  520. },
  521. outputSchema: {
  522. status: z.string(),
  523. assistantMessage: z.string(),
  524. summary: z.object({}).passthrough().optional(),
  525. data: z.object({}).passthrough().optional(),
  526. files: z.array(z.string()).optional(),
  527. nextActions: z.array(z.string()).optional(),
  528. warnings: z.array(z.any()).optional(),
  529. errors: z.array(z.any()).optional()
  530. }
  531. },
  532. async input => {
  533. const result = await runXiaohongshuTrend({
  534. ...input,
  535. collectionMode: input.collectionMode || 'sample'
  536. });
  537. return asToolResult(result);
  538. }
  539. );
  540. server.registerTool(
  541. 'voc_douyin_trend_run',
  542. {
  543. title: 'Run Douyin Trend Intelligence',
  544. description: [
  545. 'Generate first-round Douyin trend observations and validation questions from an industry profile.',
  546. 'P0 supports sample mode for demos and course recording.',
  547. 'Returns assistantMessage as the user-facing body; first-round output is a hypothesis that needs user calibration.'
  548. ].join(' '),
  549. inputSchema: {
  550. collectionMode: z.enum(['sample', 'live']).optional().describe('Use sample for P0 demo. Live will be enabled after VOC provider integration.'),
  551. profile: z.string().optional().describe('Optional profile JSON path.'),
  552. output: z.string().optional().describe('Optional output directory.'),
  553. memory: z.string().optional().describe('Optional preference memory JSON path.'),
  554. memoryPath: z.string().optional().describe('Optional preference memory JSON path.'),
  555. project: z.string().optional(),
  556. industry: z.string().optional(),
  557. businessType: z.string().optional(),
  558. targetAudience: z.array(z.string()).optional(),
  559. keywords: z.array(z.string()).optional(),
  560. trendQuestions: z.array(z.string()).optional(),
  561. mustTrackSignals: z.array(z.string()).optional(),
  562. keywordLimit: z.number().int().min(1).max(10).optional(),
  563. videosPerKeyword: z.number().int().min(1).max(10).optional(),
  564. maxCommentPages: z.number().int().min(0).max(5).optional(),
  565. sortType: z.enum(['0', '1', '2']).optional(),
  566. publishTime: z.enum(['0', '1', '7', '180']).optional(),
  567. filterDuration: z.enum(['0', '0-1', '1-5', '5-10000']).optional(),
  568. contentType: z.enum(['0', '1', '2', '3']).optional(),
  569. cacheAssets: z.boolean().optional().describe('Whether to cache evidence images into the output assets directory. Defaults to true.'),
  570. assetLimit: z.number().int().min(0).max(30).optional().describe('Maximum number of video cover images to cache.'),
  571. douyinToken: z.string().optional().describe('Optional request-scoped collection token. It is used only for this run and never echoed back.'),
  572. vocToken: z.string().optional().describe('Optional VOC social token, same convention as xiaohongshu trend. It is used only for this run and never echoed back.')
  573. },
  574. outputSchema: {
  575. status: z.string(),
  576. assistantMessage: z.string(),
  577. summary: z.object({}).passthrough().optional(),
  578. data: z.object({}).passthrough().optional(),
  579. files: z.array(z.string()).optional(),
  580. nextActions: z.array(z.string()).optional(),
  581. warnings: z.array(z.any()).optional(),
  582. errors: z.array(z.any()).optional()
  583. }
  584. },
  585. async input => {
  586. const result = await runDouyinTrend({
  587. ...input,
  588. collectionMode: input.collectionMode || 'sample'
  589. });
  590. return asToolResult(result);
  591. }
  592. );
  593. server.registerTool(
  594. 'voc_xiaohongshu_preference_update',
  595. {
  596. title: 'Update Xiaohongshu Trend Preference',
  597. description: [
  598. 'Save user feedback for the next Xiaohongshu trend refinement.',
  599. 'Use after the user answers validation questions or says what to keep, block, downgrade, or focus on next time.'
  600. ].join(' '),
  601. inputSchema: {
  602. message: z.string().optional().describe('Natural-language user feedback.'),
  603. feedback: z.string().optional().describe('Natural-language user feedback.'),
  604. memory: z.string().optional().describe('Optional preference memory JSON path.'),
  605. memoryPath: z.string().optional().describe('Optional preference memory JSON path.'),
  606. preferredDirections: z.array(z.string()).optional(),
  607. blockedDirections: z.array(z.string()).optional(),
  608. focusModes: z.array(z.string()).optional()
  609. },
  610. outputSchema: {
  611. status: z.string(),
  612. assistantMessage: z.string(),
  613. summary: z.object({}).passthrough().optional(),
  614. data: z.object({}).passthrough().optional(),
  615. files: z.array(z.string()).optional(),
  616. nextActions: z.array(z.string()).optional(),
  617. warnings: z.array(z.string()).optional(),
  618. errors: z.array(z.any()).optional()
  619. }
  620. },
  621. async input => {
  622. const result = await updateXiaohongshuPreference(input);
  623. return asToolResult(result);
  624. }
  625. );
  626. server.registerTool(
  627. 'voc_douyin_preference_update',
  628. {
  629. title: 'Update Douyin Trend Preference',
  630. description: [
  631. 'Save user feedback for the next Douyin trend refinement.',
  632. 'Use after the user answers validation questions or says what to keep, block, downgrade, or focus on next time.'
  633. ].join(' '),
  634. inputSchema: {
  635. message: z.string().optional().describe('Natural-language user feedback.'),
  636. feedback: z.string().optional().describe('Natural-language user feedback.'),
  637. memory: z.string().optional().describe('Optional preference memory JSON path.'),
  638. memoryPath: z.string().optional().describe('Optional preference memory JSON path.'),
  639. preferredDirections: z.array(z.string()).optional(),
  640. blockedDirections: z.array(z.string()).optional(),
  641. focusModes: z.array(z.string()).optional()
  642. },
  643. outputSchema: {
  644. status: z.string(),
  645. assistantMessage: z.string(),
  646. summary: z.object({}).passthrough().optional(),
  647. data: z.object({}).passthrough().optional(),
  648. files: z.array(z.string()).optional(),
  649. nextActions: z.array(z.string()).optional(),
  650. warnings: z.array(z.any()).optional(),
  651. errors: z.array(z.any()).optional()
  652. }
  653. },
  654. async input => {
  655. const result = await updateDouyinPreference(input);
  656. return asToolResult(result);
  657. }
  658. );
  659. server.registerTool(
  660. 'voc_api_search',
  661. {
  662. title: 'Search VOC Forwarding API Catalog',
  663. description: [
  664. 'Search the VOC social forwarding interface catalog (douyin, xiaohongshu, and other TikHub-backed platforms).',
  665. 'Use when the user wants to collect social data for any keyword/industry and you need to find which forwarding interface to call before reading its parameter doc and invoking it.'
  666. ].join(' '),
  667. inputSchema: {
  668. query: z.string().optional().describe('Free-text search over interface id/title/summary/tags, e.g. 抖音 评论 / search notes.'),
  669. platform: z.string().optional().describe('Optional platform filter, e.g. douyin, xiaohongshu.'),
  670. tag: z.string().optional().describe('Optional tag filter.'),
  671. limit: z.number().int().min(1).max(100).optional()
  672. },
  673. outputSchema: {
  674. status: z.string(),
  675. assistantMessage: z.string(),
  676. summary: z.object({}).passthrough().optional(),
  677. data: z.object({}).passthrough().optional(),
  678. files: z.array(z.string()).optional(),
  679. nextActions: z.array(z.string()).optional(),
  680. warnings: z.array(z.any()).optional(),
  681. errors: z.array(z.any()).optional()
  682. }
  683. },
  684. async input => asToolResult(await searchVocApis(input))
  685. );
  686. server.registerTool(
  687. 'voc_api_doc',
  688. {
  689. title: 'Read VOC Forwarding API Parameter Doc',
  690. description: [
  691. 'Read the detailed parameter documentation for one VOC forwarding interface, plus a ready-to-use call template.',
  692. 'Use after voc_api_search to learn an interface\u2019s required/optional parameters before calling voc_api_call.'
  693. ].join(' '),
  694. inputSchema: {
  695. id: z.string().optional().describe('Interface id from the catalog, e.g. douyin.search_general.'),
  696. proxyPath: z.string().optional().describe('Alternatively, the upstream proxy path, e.g. douyin/search/fetch_general_search_v2.')
  697. },
  698. outputSchema: {
  699. status: z.string(),
  700. assistantMessage: z.string(),
  701. summary: z.object({}).passthrough().optional(),
  702. data: z.object({}).passthrough().optional(),
  703. files: z.array(z.string()).optional(),
  704. nextActions: z.array(z.string()).optional(),
  705. warnings: z.array(z.any()).optional(),
  706. errors: z.array(z.any()).optional()
  707. }
  708. },
  709. async input => asToolResult(await getVocApiDoc(input))
  710. );
  711. server.registerTool(
  712. 'voc_api_call',
  713. {
  714. title: 'Call A VOC Forwarding API',
  715. description: [
  716. 'Invoke any VOC social forwarding interface and return the upstream data.',
  717. 'Provide a catalog id (preferred) or a rawPath + method for unlisted interfaces, plus a params object assembled from the interface doc.',
  718. 'Requires an r: session token (not an sk- AIGate key). Input/parameter errors, upstream instability, auth, and billing are reported distinctly and are never disguised as \u201cno data / category unsupported\u201d.'
  719. ].join(' '),
  720. inputSchema: {
  721. id: z.string().optional().describe('Catalog interface id, e.g. douyin.search_general.'),
  722. rawPath: z.string().optional().describe('Upstream proxy path for interfaces not in the catalog, e.g. douyin/search/fetch_general_search_v2.'),
  723. proxyPath: z.string().optional().describe('Alias of rawPath.'),
  724. method: z.enum(['GET', 'POST', 'PUT', 'DELETE']).optional().describe('HTTP method, only needed for rawPath calls. Defaults to GET.'),
  725. params: z.object({}).passthrough().optional().describe('Parameter object assembled from the interface doc.'),
  726. query: z.object({}).passthrough().optional().describe('Optional explicit query parameters.'),
  727. body: z.object({}).passthrough().optional().describe('Optional explicit request body.'),
  728. retries: z.number().int().min(0).max(8).optional(),
  729. baseUrl: z.string().optional(),
  730. vocToken: z.string().optional().describe('Optional VOC social r: session token. Used only for this run and never echoed back.'),
  731. token: z.string().optional().describe('Alias of vocToken. Never echoed back.'),
  732. douyinToken: z.string().optional(),
  733. xiaohongshuToken: z.string().optional()
  734. },
  735. outputSchema: {
  736. status: z.string(),
  737. assistantMessage: z.string(),
  738. summary: z.object({}).passthrough().optional(),
  739. data: z.object({}).passthrough().optional(),
  740. files: z.array(z.string()).optional(),
  741. nextActions: z.array(z.string()).optional(),
  742. warnings: z.array(z.any()).optional(),
  743. errors: z.array(z.any()).optional()
  744. }
  745. },
  746. async input => asToolResult(await callVocApi(input))
  747. );
  748. return server;
  749. }
  750. async function main() {
  751. const server = createServer();
  752. const transport = new StdioServerTransport();
  753. await server.connect(transport);
  754. }
  755. if (require.main === module) {
  756. main().catch(error => {
  757. console.error(error);
  758. process.exit(1);
  759. });
  760. }
  761. module.exports = {
  762. createServer
  763. };