smoke-mcp.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. #!/usr/bin/env node
  2. const path = require('path');
  3. const { Client } = require('@modelcontextprotocol/sdk/client/index.js');
  4. const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js');
  5. const { version } = require('../package.json');
  6. async function main() {
  7. const cwd = path.resolve(__dirname, '..');
  8. const client = new Client({
  9. name: 'voc-intelligence-smoke',
  10. version
  11. });
  12. const transport = new StdioClientTransport({
  13. command: process.execPath,
  14. args: ['mcp/src/server.js'],
  15. cwd,
  16. stderr: 'pipe'
  17. });
  18. const stderr = transport.stderr;
  19. if (stderr) {
  20. stderr.on('data', chunk => {
  21. process.stderr.write(chunk);
  22. });
  23. }
  24. await client.connect(transport);
  25. try {
  26. const tools = await client.listTools();
  27. const toolNames = tools.tools.map(tool => tool.name);
  28. const trendTool = tools.tools.find(tool => tool.name === 'voc_xiaohongshu_trend_run');
  29. const trendSchema = JSON.stringify(trendTool?.inputSchema || {});
  30. if (!trendTool || !trendSchema.includes('xiaohongshuToken') || !trendSchema.includes('vocToken')) {
  31. throw new Error('Expected voc_xiaohongshu_trend_run to accept simple request token inputs xiaohongshuToken and vocToken');
  32. }
  33. if (!toolNames.includes('voc_xiaohongshu_trend_run')) {
  34. throw new Error(`Expected tool voc_xiaohongshu_trend_run, got: ${toolNames.join(', ')}`);
  35. }
  36. if (!toolNames.includes('voc_xiaohongshu_preference_update')) {
  37. throw new Error(`Expected tool voc_xiaohongshu_preference_update, got: ${toolNames.join(', ')}`);
  38. }
  39. if (!toolNames.includes('voc_problem_deep_dive_run')) {
  40. throw new Error(`Expected tool voc_problem_deep_dive_run, got: ${toolNames.join(', ')}`);
  41. }
  42. if (!toolNames.includes('voc_issue_pool_run')) {
  43. throw new Error(`Expected tool voc_issue_pool_run, got: ${toolNames.join(', ')}`);
  44. }
  45. if (!toolNames.includes('voc_content_plan_run')) {
  46. throw new Error(`Expected tool voc_content_plan_run, got: ${toolNames.join(', ')}`);
  47. }
  48. if (!toolNames.includes('voc_speaking_script_run')) {
  49. throw new Error(`Expected tool voc_speaking_script_run, got: ${toolNames.join(', ')}`);
  50. }
  51. if (!toolNames.includes('voc_competitor_map_run')) {
  52. throw new Error(`Expected tool voc_competitor_map_run, got: ${toolNames.join(', ')}`);
  53. }
  54. if (!toolNames.includes('voc_business_workflow_run')) {
  55. throw new Error(`Expected tool voc_business_workflow_run, got: ${toolNames.join(', ')}`);
  56. }
  57. const issuePoolTool = tools.tools.find(tool => tool.name === 'voc_issue_pool_run');
  58. const issuePoolSchema = JSON.stringify(issuePoolTool?.inputSchema || {});
  59. if (!issuePoolSchema.includes('evidence') || !issuePoolSchema.includes('resolvedIssues')) {
  60. throw new Error('Expected voc_issue_pool_run to expose evidence and status inputs');
  61. }
  62. const deepDiveTool = tools.tools.find(tool => tool.name === 'voc_problem_deep_dive_run');
  63. const deepDiveSchema = JSON.stringify(deepDiveTool?.inputSchema || {});
  64. if (!deepDiveSchema.includes('project') || !deepDiveSchema.includes('feedback') || !deepDiveSchema.includes('blockedActions')) {
  65. throw new Error('Expected voc_problem_deep_dive_run to expose scoped memory and feedback inputs');
  66. }
  67. const preferenceResult = await client.callTool({
  68. name: 'voc_xiaohongshu_preference_update',
  69. arguments: {
  70. message: '保留奶油风和全屋定制翻车方向,不要纯风格美图,下一个版本更偏门店转化话术。',
  71. memory: '../outputs/claude-code-xhs-mcp-smoke/xiaohongshu-trend-memory.json'
  72. }
  73. });
  74. const preferenceStructured = preferenceResult.structuredContent || {};
  75. if (preferenceStructured.status !== 'ok') {
  76. throw new Error(`Expected ok preference result, got: ${JSON.stringify(preferenceStructured)}`);
  77. }
  78. const sentinelToken = 'SMOKE_REQUEST_TOKEN_VALUE';
  79. const tokenCheckResult = await client.callTool({
  80. name: 'voc_xiaohongshu_token_check',
  81. arguments: {
  82. vocToken: sentinelToken
  83. }
  84. });
  85. const tokenStructured = tokenCheckResult.structuredContent || {};
  86. const tokenText = JSON.stringify(tokenStructured);
  87. if (tokenStructured.status !== 'ok' || tokenStructured.configured !== true) {
  88. throw new Error(`Expected request-scoped token to be accepted, got: ${tokenText}`);
  89. }
  90. if (tokenText.includes(sentinelToken)) {
  91. throw new Error('Request-scoped token leaked in MCP token check result');
  92. }
  93. const result = await client.callTool({
  94. name: 'voc_xiaohongshu_trend_run',
  95. arguments: {
  96. collectionMode: 'sample',
  97. profile: 'memory-templates/xiaohongshu-trend-profile.json',
  98. memory: '../outputs/claude-code-xhs-mcp-smoke/xiaohongshu-trend-memory.json',
  99. output: '../outputs/claude-code-xhs-mcp-smoke'
  100. }
  101. });
  102. const structured = result.structuredContent || {};
  103. if (structured.status !== 'ok') {
  104. throw new Error(`Expected ok result, got: ${JSON.stringify(structured)}`);
  105. }
  106. const assistantMessage = String(structured.assistantMessage || '');
  107. const evidenceFields = ['## 证据样本', '原帖', '原文摘录', '高赞评论'];
  108. const missingEvidenceFields = evidenceFields.filter(field => !assistantMessage.includes(field));
  109. if (missingEvidenceFields.length) {
  110. throw new Error(`Expected evidence card fields in sample report, missing: ${missingEvidenceFields.join(', ')}`);
  111. }
  112. if (/xiaohongshu\.com\/explore\//.test(assistantMessage)) {
  113. throw new Error('Sample report should not contain /explore/ Xiaohongshu links');
  114. }
  115. const assetFiles = (structured.files || []).filter(file => /[\\/]assets[\\/]/.test(file));
  116. if (!assetFiles.length) {
  117. throw new Error('Expected cached asset files in sample report result');
  118. }
  119. const issuePoolResult = await client.callTool({
  120. name: 'voc_issue_pool_run',
  121. arguments: {
  122. project: 'MCP样板门店',
  123. industry: '本地生活餐饮',
  124. scenario: '门店经营',
  125. audience: '到店顾客',
  126. evidence: [
  127. '排队久,但是不知道值不值得等。',
  128. '人均有点高,第一次来不知道怎么点不踩雷。',
  129. '服务忙起来没人理。'
  130. ]
  131. }
  132. });
  133. const issuePoolStructured = issuePoolResult.structuredContent || {};
  134. if (issuePoolStructured.status !== 'ok' ||
  135. !String(issuePoolStructured.assistantMessage || '').includes('## 问题优先级') ||
  136. !issuePoolStructured.data?.issues?.length) {
  137. throw new Error(`Expected issue pool result, got: ${JSON.stringify(issuePoolStructured)}`);
  138. }
  139. const deepDiveResult = await client.callTool({
  140. name: 'voc_problem_deep_dive_run',
  141. arguments: {
  142. issue: '排队',
  143. project: 'MCP样板门店',
  144. industry: '本地生活餐饮',
  145. scenario: '门店经营',
  146. audience: '到店顾客',
  147. evidence: ['排队久,但是不知道值不值得等。'],
  148. feedback: '老板不想做等位饮品,更想先改评论区回复和预点单'
  149. }
  150. });
  151. const deepDiveStructured = deepDiveResult.structuredContent || {};
  152. const deepDiveMessage = String(deepDiveStructured.assistantMessage || '');
  153. if (deepDiveStructured.status !== 'ok' ||
  154. !deepDiveMessage.includes('## 可执行解决动作') ||
  155. !deepDiveMessage.includes('## 优先级判断') ||
  156. !deepDiveStructured.summary?.memoryPath) {
  157. throw new Error(`Expected boss-oriented deep dive result, got: ${JSON.stringify(deepDiveStructured)}`);
  158. }
  159. if (!deepDiveStructured.data?.memory?.blockedActions?.includes('等位饮品') ||
  160. !deepDiveStructured.data?.memory?.preferredActions?.includes('预点单')) {
  161. throw new Error('Expected deep dive MCP call to parse feedback into scoped memory');
  162. }
  163. if (!JSON.stringify(deepDiveStructured.nextActions || []).includes('live')) {
  164. throw new Error('Expected deep dive next actions to hand off to live collection instead of trapping the workflow');
  165. }
  166. const contentPlanResult = await client.callTool({
  167. name: 'voc_content_plan_run',
  168. arguments: {
  169. brand: 'MCP样板门店',
  170. industry: '本地生活餐饮',
  171. issues: ['第一次来怕踩雷', '觉得人均贵', '怕排队久']
  172. }
  173. });
  174. const contentPlanStructured = contentPlanResult.structuredContent || {};
  175. const contentPlanMessage = String(contentPlanStructured.assistantMessage || '');
  176. if (contentPlanStructured.status !== 'ok' ||
  177. !contentPlanMessage.includes('7 天 VOC 内容选题') ||
  178. !contentPlanMessage.includes('前 3 秒钩子') ||
  179. !contentPlanStructured.data?.plan?.length) {
  180. throw new Error(`Expected VOC content plan result, got: ${JSON.stringify(contentPlanStructured)}`);
  181. }
  182. const teaContentPlanResult = await client.callTool({
  183. name: 'voc_content_plan_run',
  184. arguments: {
  185. brand: '茶饮新客转化',
  186. industry: '茶饮',
  187. issues: [
  188. '第一次来怎么点最稳',
  189. '18-22 元贵在哪/值在哪',
  190. '券后价和套餐怎么买最划算',
  191. '茶底/甜度/配料怎么选',
  192. '哪几款不容易踩雷',
  193. '外卖到手怎么避免翻车',
  194. '买一送一/团购规则怎么讲清楚'
  195. ],
  196. evidenceText: '小红书评论:波霸奶茶用的什么茶底?我要避雷!!下午喝的,晚上睁眼到天亮。抖音线索:9.9 新品、11.9 单人下午茶、21 元买一送一能触发注意,但还需要解释规则、口味和是否有坑。'
  197. }
  198. });
  199. const teaContentPlanStructured = teaContentPlanResult.structuredContent || {};
  200. const teaContentPlanMessage = String(teaContentPlanStructured.assistantMessage || '');
  201. if (teaContentPlanStructured.status !== 'ok' ||
  202. !teaContentPlanMessage.includes('茶底怎么选') ||
  203. !teaContentPlanMessage.includes('买一送一到底怎么算') ||
  204. teaContentPlanMessage.includes('招牌菜') ||
  205. teaContentPlanMessage.includes('下饭菜')) {
  206. throw new Error(`Expected tea-specific VOC content plan result, got: ${JSON.stringify(teaContentPlanStructured)}`);
  207. }
  208. const speakingScriptResult = await client.callTool({
  209. name: 'voc_speaking_script_run',
  210. arguments: {
  211. brand: 'MCP样板门店',
  212. industry: '本地生活餐饮',
  213. topic: '第一次来怎么点不踩雷',
  214. userIssue: '第一次来怕点错',
  215. evidence: ['第一次来不知道怎么点,怕踩雷'],
  216. feedback: '开头更狠一点,老板视角,少讲概念,多给具体场景',
  217. memory: '../outputs/claude-code-xhs-mcp-smoke/voc-speaking-script-memory.json',
  218. finalize: true
  219. }
  220. });
  221. const speakingScriptStructured = speakingScriptResult.structuredContent || {};
  222. const speakingScriptMessage = String(speakingScriptStructured.assistantMessage || '');
  223. if (speakingScriptStructured.status !== 'ok' ||
  224. !speakingScriptMessage.includes('VOC 口播脚本共创') ||
  225. !speakingScriptMessage.includes('60 秒口播稿') ||
  226. !speakingScriptMessage.includes('30 秒压缩版') ||
  227. !speakingScriptStructured.data?.memory?.finalizedScripts?.length ||
  228. !speakingScriptStructured.data?.memory?.preferredStyles?.includes('老板视角')) {
  229. throw new Error(`Expected VOC speaking script result, got: ${JSON.stringify(speakingScriptStructured)}`);
  230. }
  231. const competitorMapResult = await client.callTool({
  232. name: 'voc_competitor_map_run',
  233. arguments: {
  234. brand: 'MCP样板门店',
  235. city: '南昌',
  236. category: '餐饮'
  237. }
  238. });
  239. const competitorMapStructured = competitorMapResult.structuredContent || {};
  240. const competitorMapMessage = String(competitorMapStructured.assistantMessage || '');
  241. if (competitorMapStructured.status !== 'ok' ||
  242. !competitorMapMessage.includes('竞品图谱') ||
  243. !competitorMapMessage.includes('错位竞争') ||
  244. !competitorMapStructured.data?.competitors?.length) {
  245. throw new Error(`Expected VOC competitor map result, got: ${JSON.stringify(competitorMapStructured)}`);
  246. }
  247. const businessWorkflowResult = await client.callTool({
  248. name: 'voc_business_workflow_run',
  249. arguments: {
  250. brand: 'MCP样板门店',
  251. industry: '本地生活餐饮',
  252. platform: 'douyin',
  253. collectionMode: 'sample',
  254. keywords: ['南昌菜', '第一次来怎么点', '排队']
  255. }
  256. });
  257. const businessWorkflowStructured = businessWorkflowResult.structuredContent || {};
  258. const businessWorkflowMessage = String(businessWorkflowStructured.assistantMessage || '');
  259. if (businessWorkflowStructured.status !== 'ok' ||
  260. !businessWorkflowMessage.includes('VOC经营闭环') ||
  261. businessWorkflowStructured.summary?.workflowStage !== 'business_workflow_first_round') {
  262. throw new Error(`Expected VOC business workflow result, got: ${JSON.stringify(businessWorkflowStructured)}`);
  263. }
  264. console.log(JSON.stringify({
  265. status: 'ok',
  266. tools: toolNames,
  267. assistantMessagePreview: assistantMessage.slice(0, 120),
  268. deepDivePreview: deepDiveMessage.slice(0, 120),
  269. contentPlanPreview: contentPlanMessage.slice(0, 120),
  270. speakingScriptPreview: speakingScriptMessage.slice(0, 120),
  271. competitorMapPreview: competitorMapMessage.slice(0, 120),
  272. businessWorkflowPreview: businessWorkflowMessage.slice(0, 120),
  273. preferenceMemoryApplied: Boolean(structured.summary && structured.summary.preferenceMemoryApplied),
  274. files: structured.files || []
  275. }, null, 2));
  276. } finally {
  277. await client.close();
  278. }
  279. }
  280. main().catch(error => {
  281. console.error(error);
  282. process.exit(1);
  283. });