| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788 |
- #!/usr/bin/env node
- const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
- const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
- const { z } = require('zod');
- const { readXiaohongshuToken, readVocToken } = require('./core/credentials');
- const { runXiaohongshuTrend } = require('./tools/xiaohongshu-trend-run');
- const { updateXiaohongshuPreference } = require('./tools/xiaohongshu-preference-update');
- const { runDouyinTrend } = require('./tools/douyin-trend-run');
- const { updateDouyinPreference } = require('./tools/douyin-preference-update');
- const { runVocProblemDeepDive } = require('./tools/voc-problem-deep-dive-run');
- const { runVocIssuePool } = require('./tools/voc-issue-pool-run');
- const { runVocContentPlan } = require('./tools/voc-content-plan-run');
- const { runVocCompetitorMap } = require('./tools/voc-competitor-map-run');
- const { runVocSpeakingScript } = require('./tools/voc-speaking-script-run');
- const { runBusinessWorkflow } = require('./tools/voc-business-workflow-run');
- const { analyzeFmodeImage } = require('./tools/fmode-image-analysis');
- const { searchVocApis, getVocApiDoc, callVocApi } = require('./tools/voc-api-catalog-run');
- const { buildVocRechargeInfo, buildMissingTokenMessage } = require('./core/payment-links');
- function asToolResult(result) {
- const normalized = normalizeToolResult(result);
- return {
- content: [
- {
- type: 'text',
- text: normalized.assistantMessage || JSON.stringify(normalized, null, 2)
- }
- ],
- structuredContent: normalized,
- isError: normalized.status !== 'ok'
- };
- }
- function normalizeWarning(value) {
- if (typeof value === 'string') return value;
- if (!value || typeof value !== 'object') return String(value || '');
- const parts = [
- value.stage,
- value.keyword && `keyword=${value.keyword}`,
- value.noteId && `noteId=${value.noteId}`,
- value.videoId && `videoId=${value.videoId}`,
- value.httpStatus && `http=${value.httpStatus}`,
- value.message
- ].filter(Boolean);
- return parts.join(' | ') || JSON.stringify(value);
- }
- function normalizeToolResult(result = {}) {
- const normalized = { ...result };
- if (Object.prototype.hasOwnProperty.call(result, 'warnings')) {
- normalized.warnings = Array.isArray(result.warnings)
- ? result.warnings.map(normalizeWarning).filter(Boolean)
- : [];
- }
- if (Object.prototype.hasOwnProperty.call(result, 'errors')) {
- normalized.errors = Array.isArray(result.errors) ? result.errors : [];
- }
- return normalized;
- }
- function createServer() {
- const server = new McpServer({
- name: 'voc-intelligence',
- version: '0.3.17'
- });
- server.registerTool(
- 'fmode_image_analysis',
- {
- title: 'Analyze Images With Fmode Doubao Vision',
- description: [
- 'Analyze screenshots, product images, UI images, charts, posters, or local image files through Fmode OpenAI-compatible Doubao vision.',
- 'Use when the current Claude Code turn includes uploaded or pasted image attachments surfaced as local temp file paths, file objects, or data URLs.',
- 'Use when the user asks for 图片识别, 识图, 看图, 分析截图, 提取图片文字, UI 截图分析, or when a text-only model such as DeepSeek cannot read an image.',
- 'Default model is doubao-seed-2-0-pro-260215 and billing uses the configured platform user token instead of a separate model key.'
- ].join(' '),
- inputSchema: {
- imagePath: z.string().optional(),
- imagePaths: z.array(z.string()).optional(),
- imageUrl: z.string().optional(),
- imageUrls: z.array(z.string()).optional(),
- image: z.union([z.string(), z.object({}).passthrough()]).optional(),
- images: z.array(z.union([z.string(), z.object({}).passthrough()])).optional(),
- file: z.union([z.string(), z.object({}).passthrough()]).optional(),
- files: z.array(z.union([z.string(), z.object({}).passthrough()])).optional(),
- attachment: z.union([z.string(), z.object({}).passthrough()]).optional(),
- attachments: z.array(z.union([z.string(), z.object({}).passthrough()])).optional(),
- prompt: z.string().optional(),
- question: z.string().optional(),
- text: z.string().optional(),
- context: z.string().optional(),
- outputFormat: z.enum(['text', 'json']).optional(),
- responseFormat: z.enum(['text', 'json']).optional(),
- detail: z.enum(['auto', 'low', 'high']).optional(),
- temperature: z.number().optional(),
- maxTokens: z.number().int().min(64).max(8192).optional(),
- maxImageBytes: z.number().int().min(1024).optional(),
- output: z.string().optional(),
- outputPath: z.string().optional(),
- outputDir: z.string().optional(),
- baseUrl: z.string().optional(),
- path: z.string().optional(),
- fmodeBaseUrl: z.string().optional(),
- fmodePath: z.string().optional(),
- fmodeModel: z.string().optional(),
- model: z.string().optional(),
- doubaoVisionBaseUrl: z.string().optional(),
- doubaoVisionPath: z.string().optional(),
- doubaoVisionModel: z.string().optional(),
- videoAnalysisBaseUrl: z.string().optional(),
- videoAnalysisPath: z.string().optional(),
- videoAnalysisModel: z.string().optional(),
- fmodeToken: z.string().optional(),
- imageAnalysisToken: z.string().optional(),
- doubaoVisionToken: z.string().optional(),
- videoAnalysisToken: z.string().optional(),
- xiaohongshuToken: z.string().optional(),
- douyinToken: z.string().optional(),
- vocToken: z.string().optional(),
- tihaoToken: z.string().optional(),
- sessionToken: z.string().optional(),
- token: z.string().optional(),
- apiToken: z.string().optional(),
- allowEnvToken: z.boolean().optional()
- },
- outputSchema: {
- status: z.string(),
- assistantMessage: z.string(),
- summary: z.object({}).passthrough().optional(),
- data: z.object({}).passthrough().optional(),
- files: z.array(z.string()).optional(),
- nextActions: z.array(z.string()).optional(),
- warnings: z.array(z.any()).optional(),
- errors: z.array(z.any()).optional()
- }
- },
- async input => asToolResult(await analyzeFmodeImage(input))
- );
- server.registerTool(
- 'voc_business_workflow_run',
- {
- title: 'Run VOC Business Workflow',
- description: [
- 'Run the full VOC business workflow from market voices to issue pool, top issue deep dive, 7-day content plan, and one speaking script.',
- '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.'
- ].join(' '),
- inputSchema: {
- project: z.string().optional(),
- brand: z.string().optional(),
- store: z.string().optional(),
- industry: z.string().optional(),
- category: z.string().optional(),
- platform: z.enum(['douyin', 'xiaohongshu', '\u6296\u97f3', '\u5c0f\u7ea2\u4e66']).optional(),
- collectionMode: z.enum(['sample', 'live']).optional(),
- scenario: z.string().optional(),
- scene: z.string().optional(),
- audience: z.string().optional(),
- targetAudience: z.string().optional(),
- keywords: z.array(z.string()).optional(),
- keyword: z.string().optional(),
- issue: z.string().optional(),
- problem: z.string().optional(),
- topic: z.string().optional(),
- feedback: z.string().optional(),
- finalize: z.boolean().optional(),
- output: z.string().optional(),
- keywordLimit: z.number().int().min(1).max(10).optional(),
- notesPerKeyword: z.number().int().min(1).max(10).optional(),
- videosPerKeyword: z.number().int().min(1).max(10).optional(),
- maxCommentPages: z.number().int().min(0).max(5).optional(),
- xiaohongshuToken: z.string().optional(),
- douyinToken: z.string().optional(),
- vocToken: z.string().optional()
- },
- outputSchema: {
- status: z.string(),
- assistantMessage: z.string(),
- summary: z.object({}).passthrough().optional(),
- data: z.object({}).passthrough().optional(),
- files: z.array(z.string()).optional(),
- nextActions: z.array(z.string()).optional(),
- warnings: z.array(z.any()).optional(),
- errors: z.array(z.any()).optional()
- }
- },
- async input => asToolResult(await runBusinessWorkflow(input))
- );
- server.registerTool(
- 'voc_speaking_script_run',
- {
- title: 'Run VOC Speaking Script Co-Creation',
- description: [
- 'Turn one VOC-backed topic into a co-created speaking script.',
- '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.'
- ].join(' '),
- inputSchema: {
- project: z.string().optional(),
- brand: z.string().optional(),
- store: z.string().optional(),
- industry: z.string().optional(),
- category: z.string().optional(),
- topic: z.string().optional(),
- title: z.string().optional(),
- selectedTopic: z.string().optional(),
- userIssue: z.string().optional(),
- issue: z.string().optional(),
- problem: z.string().optional(),
- issues: z.array(z.string()).optional(),
- problems: z.array(z.string()).optional(),
- vocIssues: z.array(z.string()).optional(),
- evidence: z.array(z.string()).optional(),
- comments: z.array(z.string()).optional(),
- voices: z.array(z.string()).optional(),
- vocEvidence: z.array(z.string()).optional(),
- evidenceText: z.string().optional(),
- feedback: z.string().optional(),
- message: z.string().optional(),
- preferredStyles: z.array(z.string()).optional(),
- blockedStyles: z.array(z.string()).optional(),
- finalize: z.boolean().optional(),
- memory: z.string().optional(),
- memoryPath: z.string().optional(),
- output: z.string().optional()
- },
- outputSchema: {
- status: z.string(),
- assistantMessage: z.string(),
- summary: z.object({}).passthrough().optional(),
- data: z.object({}).passthrough().optional(),
- files: z.array(z.string()).optional(),
- nextActions: z.array(z.string()).optional(),
- warnings: z.array(z.any()).optional(),
- errors: z.array(z.any()).optional()
- }
- },
- async input => asToolResult(await runVocSpeakingScript(input))
- );
- server.registerTool(
- 'voc_content_plan_run',
- {
- title: 'Run VOC Content Plan',
- description: [
- 'Turn VOC issues, comments, trend reports, or issue-pool findings into a 7-day content plan and speaking scripts.',
- '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.'
- ].join(' '),
- inputSchema: {
- project: z.string().optional(),
- brand: z.string().optional(),
- store: z.string().optional(),
- industry: z.string().optional(),
- category: z.string().optional(),
- platform: z.string().optional(),
- days: z.number().int().min(1).max(15).optional(),
- issues: z.array(z.string()).optional(),
- problems: z.array(z.string()).optional(),
- vocIssues: z.array(z.string()).optional(),
- contentDirections: z.array(z.string()).optional(),
- directions: z.array(z.string()).optional(),
- evidence: z.array(z.string()).optional(),
- comments: z.array(z.string()).optional(),
- evidenceText: z.string().optional(),
- reportText: z.string().optional(),
- context: z.string().optional(),
- background: z.string().optional(),
- businessGoal: z.string().optional(),
- priceBand: z.string().optional(),
- targetAudience: z.string().optional(),
- report: z.string().optional(),
- reportPath: z.string().optional()
- },
- outputSchema: {
- status: z.string(),
- assistantMessage: z.string(),
- summary: z.object({}).passthrough().optional(),
- data: z.object({}).passthrough().optional(),
- files: z.array(z.string()).optional(),
- nextActions: z.array(z.string()).optional(),
- warnings: z.array(z.any()).optional(),
- errors: z.array(z.any()).optional()
- }
- },
- async input => asToolResult(await runVocContentPlan(input))
- );
- server.registerTool(
- 'voc_competitor_map_run',
- {
- title: 'Run VOC Competitor Map',
- description: [
- 'Build a competitor map and differentiated opportunity report from category, city, competitors, and VOC evidence.',
- 'Use when the user asks to look at competitors, competitor analysis, who else is doing well, or how the brand should compete differently.'
- ].join(' '),
- inputSchema: {
- project: z.string().optional(),
- brand: z.string().optional(),
- store: z.string().optional(),
- industry: z.string().optional(),
- category: z.string().optional(),
- city: z.string().optional(),
- region: z.string().optional(),
- priceBand: z.string().optional(),
- price: z.string().optional(),
- scenario: z.string().optional(),
- scene: z.string().optional(),
- competitors: z.array(z.string()).optional(),
- competitorNames: z.array(z.string()).optional(),
- evidence: z.array(z.string()).optional(),
- comments: z.array(z.string()).optional(),
- voices: z.array(z.string()).optional(),
- reportText: z.string().optional(),
- report: z.string().optional(),
- reportPath: z.string().optional()
- },
- outputSchema: {
- status: z.string(),
- assistantMessage: z.string(),
- summary: z.object({}).passthrough().optional(),
- data: z.object({}).passthrough().optional(),
- files: z.array(z.string()).optional(),
- nextActions: z.array(z.string()).optional(),
- warnings: z.array(z.any()).optional(),
- errors: z.array(z.any()).optional()
- }
- },
- async input => asToolResult(await runVocCompetitorMap(input))
- );
- server.registerTool(
- 'voc_issue_pool_run',
- {
- title: 'Run VOC Issue Pool',
- description: [
- 'Turn user comments, report text, or VOC snippets into a prioritized issue pool.',
- 'Use when the user asks which real problems matter most, asks to manage VOC issues, or wants a problem list before deep-diving.'
- ].join(' '),
- inputSchema: {
- project: z.string().optional().describe('Optional project, brand, account, or store name used to isolate issue-pool memory.'),
- brand: z.string().optional(),
- store: z.string().optional(),
- industry: z.string().optional(),
- category: z.string().optional(),
- scenario: z.string().optional(),
- scene: z.string().optional(),
- businessType: z.string().optional(),
- audience: z.string().optional(),
- targetAudience: z.string().optional(),
- evidence: z.array(z.string()).optional().describe('User comments or VOC snippets.'),
- comments: z.array(z.string()).optional().describe('User comment snippets.'),
- voices: z.array(z.string()).optional().describe('User voice snippets.'),
- issues: z.array(z.string()).optional().describe('Known issue snippets.'),
- issueTexts: z.array(z.string()).optional().describe('Known issue snippets.'),
- evidenceText: z.string().optional().describe('Raw comments or evidence text.'),
- reportText: z.string().optional().describe('Previous report markdown text.'),
- report: z.string().optional().describe('Optional previous report markdown path.'),
- reportPath: z.string().optional().describe('Optional previous report markdown path.'),
- memory: z.string().optional().describe('Optional issue-pool memory JSON path.'),
- memoryPath: z.string().optional().describe('Optional issue-pool memory JSON path.'),
- feedback: z.string().optional().describe('User feedback or status notes.'),
- message: z.string().optional().describe('User feedback or status notes.'),
- resolvedIssues: z.array(z.string()).optional().describe('Issue names or ids already resolved.'),
- validatingIssues: z.array(z.string()).optional().describe('Issue names or ids currently validating.'),
- blockedIssues: z.array(z.string()).optional().describe('Issue names or ids to pause.'),
- statusUpdates: z.record(z.string()).optional().describe('Map of issue title/id to status.')
- },
- outputSchema: {
- status: z.string(),
- assistantMessage: z.string(),
- summary: z.object({}).passthrough().optional(),
- data: z.object({}).passthrough().optional(),
- files: z.array(z.string()).optional(),
- nextActions: z.array(z.string()).optional(),
- warnings: z.array(z.any()).optional(),
- errors: z.array(z.any()).optional()
- }
- },
- async input => {
- const result = await runVocIssuePool(input);
- return asToolResult(result);
- }
- );
- server.registerTool(
- 'voc_problem_deep_dive_run',
- {
- title: 'Run VOC Problem Deep Dive',
- description: [
- 'Deep-dive a single VOC problem from a boss/operator perspective.',
- 'Use when the user says continue digging, what should the boss do, how to solve this problem, or turn this VOC into actions.'
- ].join(' '),
- inputSchema: {
- issue: z.string().optional().describe('Single VOC issue to deep dive, e.g. 排队, 贵, 不好吃, 服务差.'),
- problem: z.string().optional().describe('Alias for issue.'),
- topic: z.string().optional().describe('Alias for issue.'),
- project: z.string().optional().describe('Optional project, brand, or account name used to isolate deep-dive memory.'),
- brand: z.string().optional().describe('Optional brand name used to isolate deep-dive memory.'),
- store: z.string().optional().describe('Optional store name used to isolate deep-dive memory.'),
- industry: z.string().optional(),
- category: z.string().optional(),
- scenario: z.string().optional(),
- scene: z.string().optional(),
- businessType: z.string().optional(),
- audience: z.string().optional(),
- targetAudience: z.string().optional(),
- evidence: z.array(z.string()).optional().describe('Optional user comments or VOC snippets.'),
- evidenceText: z.string().optional().describe('Optional text with comments or evidence.'),
- comments: z.array(z.string()).optional().describe('Optional comment snippets.'),
- voices: z.array(z.string()).optional().describe('Optional user voice snippets.'),
- report: z.string().optional().describe('Optional previous report markdown path.'),
- reportPath: z.string().optional().describe('Optional previous report markdown path.'),
- memory: z.string().optional().describe('Optional deep-dive memory JSON path.'),
- memoryPath: z.string().optional().describe('Optional deep-dive memory JSON path.'),
- feedback: z.string().optional().describe('User feedback from previous iteration.'),
- message: z.string().optional().describe('User feedback from previous iteration.'),
- preferredActions: z.array(z.string()).optional().describe('Actions the user prefers.'),
- blockedActions: z.array(z.string()).optional().describe('Actions the user does not want.'),
- preferredContentAngles: z.array(z.string()).optional().describe('Content angles the user prefers.'),
- validatedActions: z.array(z.string()).optional().describe('Actions proven useful in practice.'),
- rejectedActions: z.array(z.string()).optional().describe('Actions proven ineffective in practice.')
- },
- outputSchema: {
- status: z.string(),
- assistantMessage: z.string(),
- summary: z.object({}).passthrough().optional(),
- data: z.object({}).passthrough().optional(),
- files: z.array(z.string()).optional(),
- nextActions: z.array(z.string()).optional(),
- warnings: z.array(z.any()).optional(),
- errors: z.array(z.any()).optional()
- }
- },
- async input => {
- const result = await runVocProblemDeepDive(input);
- return asToolResult(result);
- }
- );
- server.registerTool(
- 'voc_xiaohongshu_token_check',
- {
- title: 'Check Xiaohongshu Token',
- description: 'Check whether a Xiaohongshu/TikHub API token is configured for live collection.',
- inputSchema: {
- xiaohongshuToken: z.string().optional().describe('Optional request-scoped collection token. It is never echoed back.'),
- vocToken: z.string().optional().describe('Optional VOC social token, same convention as douyin-speaking-daily. It is never echoed back.')
- },
- outputSchema: {
- status: z.string(),
- assistantMessage: z.string(),
- configured: z.boolean(),
- data: z.object({}).passthrough().optional()
- }
- },
- async input => {
- const token = readXiaohongshuToken(input);
- const recharge = token ? null : await buildVocRechargeInfo();
- const result = {
- status: token ? 'ok' : 'needs_token',
- configured: Boolean(token),
- data: recharge ? { recharge } : {},
- assistantMessage: token
- ? '\u5c0f\u7ea2\u4e66 live \u91c7\u96c6 Token \u5df2\u914d\u7f6e\uff0c\u53ef\u4ee5\u5c1d\u8bd5\u5c0f\u89c4\u6a21\u771f\u5b9e\u91c7\u96c6\u3002'
- : buildMissingTokenMessage(recharge.paymentUrl)
- };
- return asToolResult(result);
- }
- );
- server.registerTool(
- 'voc_douyin_token_check',
- {
- title: 'Check Douyin Token',
- description: 'Check whether a Douyin/VOC API token is configured for live collection.',
- inputSchema: {
- douyinToken: z.string().optional().describe('Optional request-scoped Douyin token. It is never echoed back.'),
- vocToken: z.string().optional().describe('Optional VOC social token, same convention as xiaohongshu trend.'),
- xiaohongshuToken: z.string().optional().describe('Optional platform token alias, never echoed back.')
- },
- outputSchema: {
- status: z.string(),
- assistantMessage: z.string(),
- configured: z.boolean(),
- data: z.object({}).passthrough().optional()
- }
- },
- async input => {
- const token = readVocToken(input);
- const recharge = token ? null : await buildVocRechargeInfo();
- const result = {
- status: token ? 'ok' : 'needs_token',
- configured: Boolean(token),
- data: recharge ? { recharge } : {},
- assistantMessage: token
- ? '\u6296\u97f3 live \u91c7\u96c6 Token \u5df2\u914d\u7f6e\uff0c\u53ef\u4ee5\u5f00\u59cb\u5c0f\u89c4\u6a21\u771f\u5b9e\u91c7\u96c6\u3002'
- : buildMissingTokenMessage(recharge.paymentUrl, recharge, { platformLabel: '\u6296\u97f3' })
- };
- return asToolResult(result);
- }
- );
- server.registerTool(
- 'voc_xiaohongshu_trend_run',
- {
- title: 'Run Xiaohongshu Trend Intelligence',
- description: [
- 'Generate first-round Xiaohongshu trend observations and validation questions from an industry profile.',
- 'P0 supports sample mode for demos and course recording.',
- 'Returns assistantMessage as the user-facing body; first-round output is a hypothesis that needs user calibration.'
- ].join(' '),
- inputSchema: {
- collectionMode: z.enum(['sample', 'live']).optional().describe('Use sample for P0 demo. Live will be enabled after VOC provider integration.'),
- profile: z.string().optional().describe('Optional profile JSON path.'),
- output: z.string().optional().describe('Optional output directory.'),
- memory: z.string().optional().describe('Optional preference memory JSON path.'),
- memoryPath: z.string().optional().describe('Optional preference memory JSON path.'),
- project: z.string().optional(),
- industry: z.string().optional(),
- businessType: z.string().optional(),
- targetAudience: z.array(z.string()).optional(),
- keywords: z.array(z.string()).optional(),
- trendQuestions: z.array(z.string()).optional(),
- mustTrackSignals: z.array(z.string()).optional(),
- keywordLimit: z.number().int().min(1).max(10).optional(),
- notesPerKeyword: z.number().int().min(1).max(10).optional(),
- maxCommentPages: z.number().int().min(0).max(5).optional(),
- sort: z.enum(['general', 'time_descending', 'popularity_descending']).optional(),
- noteType: z.enum(['_0', '_1', '_2']).optional(),
- cacheAssets: z.boolean().optional().describe('Whether to cache evidence images into the output assets directory. Defaults to true.'),
- assetLimit: z.number().int().min(0).max(30).optional().describe('Maximum number of note cover images to cache.'),
- xiaohongshuToken: z.string().optional().describe('Optional request-scoped collection token. It is used only for this run and never echoed back.'),
- 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.')
- },
- outputSchema: {
- status: z.string(),
- assistantMessage: z.string(),
- summary: z.object({}).passthrough().optional(),
- data: z.object({}).passthrough().optional(),
- files: z.array(z.string()).optional(),
- nextActions: z.array(z.string()).optional(),
- warnings: z.array(z.any()).optional(),
- errors: z.array(z.any()).optional()
- }
- },
- async input => {
- const result = await runXiaohongshuTrend({
- ...input,
- collectionMode: input.collectionMode || 'sample'
- });
- return asToolResult(result);
- }
- );
- server.registerTool(
- 'voc_douyin_trend_run',
- {
- title: 'Run Douyin Trend Intelligence',
- description: [
- 'Generate first-round Douyin trend observations and validation questions from an industry profile.',
- 'P0 supports sample mode for demos and course recording.',
- 'Returns assistantMessage as the user-facing body; first-round output is a hypothesis that needs user calibration.'
- ].join(' '),
- inputSchema: {
- collectionMode: z.enum(['sample', 'live']).optional().describe('Use sample for P0 demo. Live will be enabled after VOC provider integration.'),
- profile: z.string().optional().describe('Optional profile JSON path.'),
- output: z.string().optional().describe('Optional output directory.'),
- memory: z.string().optional().describe('Optional preference memory JSON path.'),
- memoryPath: z.string().optional().describe('Optional preference memory JSON path.'),
- project: z.string().optional(),
- industry: z.string().optional(),
- businessType: z.string().optional(),
- targetAudience: z.array(z.string()).optional(),
- keywords: z.array(z.string()).optional(),
- trendQuestions: z.array(z.string()).optional(),
- mustTrackSignals: z.array(z.string()).optional(),
- keywordLimit: z.number().int().min(1).max(10).optional(),
- videosPerKeyword: z.number().int().min(1).max(10).optional(),
- maxCommentPages: z.number().int().min(0).max(5).optional(),
- sortType: z.enum(['0', '1', '2']).optional(),
- publishTime: z.enum(['0', '1', '7', '180']).optional(),
- filterDuration: z.enum(['0', '0-1', '1-5', '5-10000']).optional(),
- contentType: z.enum(['0', '1', '2', '3']).optional(),
- cacheAssets: z.boolean().optional().describe('Whether to cache evidence images into the output assets directory. Defaults to true.'),
- assetLimit: z.number().int().min(0).max(30).optional().describe('Maximum number of video cover images to cache.'),
- douyinToken: z.string().optional().describe('Optional request-scoped collection token. It is used only for this run and never echoed back.'),
- 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.')
- },
- outputSchema: {
- status: z.string(),
- assistantMessage: z.string(),
- summary: z.object({}).passthrough().optional(),
- data: z.object({}).passthrough().optional(),
- files: z.array(z.string()).optional(),
- nextActions: z.array(z.string()).optional(),
- warnings: z.array(z.any()).optional(),
- errors: z.array(z.any()).optional()
- }
- },
- async input => {
- const result = await runDouyinTrend({
- ...input,
- collectionMode: input.collectionMode || 'sample'
- });
- return asToolResult(result);
- }
- );
- server.registerTool(
- 'voc_xiaohongshu_preference_update',
- {
- title: 'Update Xiaohongshu Trend Preference',
- description: [
- 'Save user feedback for the next Xiaohongshu trend refinement.',
- 'Use after the user answers validation questions or says what to keep, block, downgrade, or focus on next time.'
- ].join(' '),
- inputSchema: {
- message: z.string().optional().describe('Natural-language user feedback.'),
- feedback: z.string().optional().describe('Natural-language user feedback.'),
- memory: z.string().optional().describe('Optional preference memory JSON path.'),
- memoryPath: z.string().optional().describe('Optional preference memory JSON path.'),
- preferredDirections: z.array(z.string()).optional(),
- blockedDirections: z.array(z.string()).optional(),
- focusModes: z.array(z.string()).optional()
- },
- outputSchema: {
- status: z.string(),
- assistantMessage: z.string(),
- summary: z.object({}).passthrough().optional(),
- data: z.object({}).passthrough().optional(),
- files: z.array(z.string()).optional(),
- nextActions: z.array(z.string()).optional(),
- warnings: z.array(z.string()).optional(),
- errors: z.array(z.any()).optional()
- }
- },
- async input => {
- const result = await updateXiaohongshuPreference(input);
- return asToolResult(result);
- }
- );
- server.registerTool(
- 'voc_douyin_preference_update',
- {
- title: 'Update Douyin Trend Preference',
- description: [
- 'Save user feedback for the next Douyin trend refinement.',
- 'Use after the user answers validation questions or says what to keep, block, downgrade, or focus on next time.'
- ].join(' '),
- inputSchema: {
- message: z.string().optional().describe('Natural-language user feedback.'),
- feedback: z.string().optional().describe('Natural-language user feedback.'),
- memory: z.string().optional().describe('Optional preference memory JSON path.'),
- memoryPath: z.string().optional().describe('Optional preference memory JSON path.'),
- preferredDirections: z.array(z.string()).optional(),
- blockedDirections: z.array(z.string()).optional(),
- focusModes: z.array(z.string()).optional()
- },
- outputSchema: {
- status: z.string(),
- assistantMessage: z.string(),
- summary: z.object({}).passthrough().optional(),
- data: z.object({}).passthrough().optional(),
- files: z.array(z.string()).optional(),
- nextActions: z.array(z.string()).optional(),
- warnings: z.array(z.any()).optional(),
- errors: z.array(z.any()).optional()
- }
- },
- async input => {
- const result = await updateDouyinPreference(input);
- return asToolResult(result);
- }
- );
- server.registerTool(
- 'voc_api_search',
- {
- title: 'Search VOC Forwarding API Catalog',
- description: [
- 'Search the VOC social forwarding interface catalog (douyin, xiaohongshu, and other TikHub-backed platforms).',
- '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.'
- ].join(' '),
- inputSchema: {
- query: z.string().optional().describe('Free-text search over interface id/title/summary/tags, e.g. 抖音 评论 / search notes.'),
- platform: z.string().optional().describe('Optional platform filter, e.g. douyin, xiaohongshu.'),
- tag: z.string().optional().describe('Optional tag filter.'),
- limit: z.number().int().min(1).max(100).optional()
- },
- outputSchema: {
- status: z.string(),
- assistantMessage: z.string(),
- summary: z.object({}).passthrough().optional(),
- data: z.object({}).passthrough().optional(),
- files: z.array(z.string()).optional(),
- nextActions: z.array(z.string()).optional(),
- warnings: z.array(z.any()).optional(),
- errors: z.array(z.any()).optional()
- }
- },
- async input => asToolResult(await searchVocApis(input))
- );
- server.registerTool(
- 'voc_api_doc',
- {
- title: 'Read VOC Forwarding API Parameter Doc',
- description: [
- 'Read the detailed parameter documentation for one VOC forwarding interface, plus a ready-to-use call template.',
- 'Use after voc_api_search to learn an interface\u2019s required/optional parameters before calling voc_api_call.'
- ].join(' '),
- inputSchema: {
- id: z.string().optional().describe('Interface id from the catalog, e.g. douyin.search_general.'),
- proxyPath: z.string().optional().describe('Alternatively, the upstream proxy path, e.g. douyin/search/fetch_general_search_v2.')
- },
- outputSchema: {
- status: z.string(),
- assistantMessage: z.string(),
- summary: z.object({}).passthrough().optional(),
- data: z.object({}).passthrough().optional(),
- files: z.array(z.string()).optional(),
- nextActions: z.array(z.string()).optional(),
- warnings: z.array(z.any()).optional(),
- errors: z.array(z.any()).optional()
- }
- },
- async input => asToolResult(await getVocApiDoc(input))
- );
- server.registerTool(
- 'voc_api_call',
- {
- title: 'Call A VOC Forwarding API',
- description: [
- 'Invoke any VOC social forwarding interface and return the upstream data.',
- 'Provide a catalog id (preferred) or a rawPath + method for unlisted interfaces, plus a params object assembled from the interface doc.',
- '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.'
- ].join(' '),
- inputSchema: {
- id: z.string().optional().describe('Catalog interface id, e.g. douyin.search_general.'),
- rawPath: z.string().optional().describe('Upstream proxy path for interfaces not in the catalog, e.g. douyin/search/fetch_general_search_v2.'),
- proxyPath: z.string().optional().describe('Alias of rawPath.'),
- method: z.enum(['GET', 'POST', 'PUT', 'DELETE']).optional().describe('HTTP method, only needed for rawPath calls. Defaults to GET.'),
- params: z.object({}).passthrough().optional().describe('Parameter object assembled from the interface doc.'),
- query: z.object({}).passthrough().optional().describe('Optional explicit query parameters.'),
- body: z.object({}).passthrough().optional().describe('Optional explicit request body.'),
- retries: z.number().int().min(0).max(8).optional(),
- baseUrl: z.string().optional(),
- vocToken: z.string().optional().describe('Optional VOC social r: session token. Used only for this run and never echoed back.'),
- token: z.string().optional().describe('Alias of vocToken. Never echoed back.'),
- douyinToken: z.string().optional(),
- xiaohongshuToken: z.string().optional()
- },
- outputSchema: {
- status: z.string(),
- assistantMessage: z.string(),
- summary: z.object({}).passthrough().optional(),
- data: z.object({}).passthrough().optional(),
- files: z.array(z.string()).optional(),
- nextActions: z.array(z.string()).optional(),
- warnings: z.array(z.any()).optional(),
- errors: z.array(z.any()).optional()
- }
- },
- async input => asToolResult(await callVocApi(input))
- );
- return server;
- }
- async function main() {
- const server = createServer();
- const transport = new StdioServerTransport();
- await server.connect(transport);
- }
- if (require.main === module) {
- main().catch(error => {
- console.error(error);
- process.exit(1);
- });
- }
- module.exports = {
- createServer
- };
|