smoke-mcp.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  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. if (!toolNames.includes('fmode_image_analysis')) {
  29. throw new Error(`Expected tool fmode_image_analysis, got: ${toolNames.join(', ')}`);
  30. }
  31. const imageTool = tools.tools.find(tool => tool.name === 'fmode_image_analysis');
  32. const imageSchema = JSON.stringify(imageTool?.inputSchema || {});
  33. if (!imageSchema.includes('imagePath') || !imageSchema.includes('fmodeToken')) {
  34. throw new Error('Expected fmode_image_analysis to accept imagePath and fmodeToken inputs');
  35. }
  36. const trendTool = tools.tools.find(tool => tool.name === 'voc_xiaohongshu_trend_run');
  37. const trendSchema = JSON.stringify(trendTool?.inputSchema || {});
  38. if (!trendTool || !trendSchema.includes('xiaohongshuToken') || !trendSchema.includes('vocToken')) {
  39. throw new Error('Expected voc_xiaohongshu_trend_run to accept simple request token inputs xiaohongshuToken and vocToken');
  40. }
  41. if (!toolNames.includes('voc_xiaohongshu_trend_run')) {
  42. throw new Error(`Expected tool voc_xiaohongshu_trend_run, got: ${toolNames.join(', ')}`);
  43. }
  44. if (!toolNames.includes('voc_xiaohongshu_preference_update')) {
  45. throw new Error(`Expected tool voc_xiaohongshu_preference_update, got: ${toolNames.join(', ')}`);
  46. }
  47. if (!toolNames.includes('voc_problem_deep_dive_run')) {
  48. throw new Error(`Expected tool voc_problem_deep_dive_run, got: ${toolNames.join(', ')}`);
  49. }
  50. if (!toolNames.includes('voc_issue_pool_run')) {
  51. throw new Error(`Expected tool voc_issue_pool_run, got: ${toolNames.join(', ')}`);
  52. }
  53. if (!toolNames.includes('voc_content_plan_run')) {
  54. throw new Error(`Expected tool voc_content_plan_run, got: ${toolNames.join(', ')}`);
  55. }
  56. if (!toolNames.includes('voc_speaking_script_run')) {
  57. throw new Error(`Expected tool voc_speaking_script_run, got: ${toolNames.join(', ')}`);
  58. }
  59. if (!toolNames.includes('voc_competitor_map_run')) {
  60. throw new Error(`Expected tool voc_competitor_map_run, got: ${toolNames.join(', ')}`);
  61. }
  62. if (!toolNames.includes('voc_business_workflow_run')) {
  63. throw new Error(`Expected tool voc_business_workflow_run, got: ${toolNames.join(', ')}`);
  64. }
  65. const issuePoolTool = tools.tools.find(tool => tool.name === 'voc_issue_pool_run');
  66. const issuePoolSchema = JSON.stringify(issuePoolTool?.inputSchema || {});
  67. if (!issuePoolSchema.includes('evidence') || !issuePoolSchema.includes('resolvedIssues')) {
  68. throw new Error('Expected voc_issue_pool_run to expose evidence and status inputs');
  69. }
  70. const deepDiveTool = tools.tools.find(tool => tool.name === 'voc_problem_deep_dive_run');
  71. const deepDiveSchema = JSON.stringify(deepDiveTool?.inputSchema || {});
  72. if (!deepDiveSchema.includes('project') || !deepDiveSchema.includes('feedback') || !deepDiveSchema.includes('blockedActions')) {
  73. throw new Error('Expected voc_problem_deep_dive_run to expose scoped memory and feedback inputs');
  74. }
  75. const preferenceResult = await client.callTool({
  76. name: 'voc_xiaohongshu_preference_update',
  77. arguments: {
  78. message: '保留奶油风和全屋定制翻车方向,不要纯风格美图,下一个版本更偏门店转化话术。',
  79. memory: '../outputs/claude-code-xhs-mcp-smoke/xiaohongshu-trend-memory.json'
  80. }
  81. });
  82. const preferenceStructured = preferenceResult.structuredContent || {};
  83. if (preferenceStructured.status !== 'ok') {
  84. throw new Error(`Expected ok preference result, got: ${JSON.stringify(preferenceStructured)}`);
  85. }
  86. const sentinelToken = 'SMOKE_REQUEST_TOKEN_VALUE';
  87. const tokenCheckResult = await client.callTool({
  88. name: 'voc_xiaohongshu_token_check',
  89. arguments: {
  90. vocToken: sentinelToken
  91. }
  92. });
  93. const tokenStructured = tokenCheckResult.structuredContent || {};
  94. const tokenText = JSON.stringify(tokenStructured);
  95. if (tokenStructured.status !== 'ok' || tokenStructured.configured !== true) {
  96. throw new Error(`Expected request-scoped token to be accepted, got: ${tokenText}`);
  97. }
  98. if (tokenText.includes(sentinelToken)) {
  99. throw new Error('Request-scoped token leaked in MCP token check result');
  100. }
  101. const imageNoToken = await client.callTool({
  102. name: 'fmode_image_analysis',
  103. arguments: {
  104. images: ['data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII='],
  105. prompt: '识别这张测试图片',
  106. allowEnvToken: false
  107. }
  108. });
  109. if (!imageNoToken.structuredContent || imageNoToken.structuredContent.status !== 'needs_token') {
  110. throw new Error('image analysis no-token path returned unexpected structured content');
  111. }
  112. if (Array.isArray(imageNoToken.structuredContent.errors) && imageNoToken.structuredContent.errors.length !== 0) {
  113. throw new Error('image analysis no-token path should keep errors empty');
  114. }
  115. const result = await client.callTool({
  116. name: 'voc_xiaohongshu_trend_run',
  117. arguments: {
  118. collectionMode: 'sample',
  119. cacheAssets: false,
  120. profile: 'memory-templates/xiaohongshu-trend-profile.json',
  121. memory: '../outputs/claude-code-xhs-mcp-smoke/xiaohongshu-trend-memory.json',
  122. output: '../outputs/claude-code-xhs-mcp-smoke'
  123. }
  124. });
  125. const structured = result.structuredContent || {};
  126. if (structured.status !== 'ok') {
  127. throw new Error(`Expected ok result, got: ${JSON.stringify(structured)}`);
  128. }
  129. const assistantMessage = String(structured.assistantMessage || '');
  130. const evidenceFields = ['## 证据样本', '原帖', '原文摘录', '高赞评论'];
  131. const missingEvidenceFields = evidenceFields.filter(field => !assistantMessage.includes(field));
  132. if (missingEvidenceFields.length) {
  133. throw new Error(`Expected evidence card fields in sample report, missing: ${missingEvidenceFields.join(', ')}`);
  134. }
  135. if (/xiaohongshu\.com\/explore\//.test(assistantMessage)) {
  136. throw new Error('Sample report should not contain /explore/ Xiaohongshu links');
  137. }
  138. const assetFiles = (structured.files || []).filter(file => /[\\/]assets[\\/]/.test(file));
  139. if (!assetFiles.length) {
  140. throw new Error('Expected cached asset files in sample report result');
  141. }
  142. if (!assetFiles.some(file => /media-manifest\.json$/i.test(file)) ||
  143. !(structured.warnings || []).some(warning => String(warning).includes('cacheAssets=false'))) {
  144. throw new Error('Expected sample media manifest and deprecated cacheAssets warning');
  145. }
  146. const issuePoolResult = await client.callTool({
  147. name: 'voc_issue_pool_run',
  148. arguments: {
  149. project: 'MCP示例品牌',
  150. industry: '家居定制',
  151. scenario: '线上获客',
  152. audience: '意向用户',
  153. evidence: [
  154. '第一次买不知道怎么选,怕踩雷。',
  155. '价格有点高,不确定值不值。',
  156. '咨询的时候没人理。'
  157. ]
  158. }
  159. });
  160. const issuePoolStructured = issuePoolResult.structuredContent || {};
  161. if (issuePoolStructured.status !== 'ok' ||
  162. !String(issuePoolStructured.assistantMessage || '').includes('## 问题优先级') ||
  163. !issuePoolStructured.data?.issues?.length) {
  164. throw new Error(`Expected issue pool result, got: ${JSON.stringify(issuePoolStructured)}`);
  165. }
  166. const deepDiveResult = await client.callTool({
  167. name: 'voc_problem_deep_dive_run',
  168. arguments: {
  169. issue: '怎么选',
  170. project: 'MCP示例品牌',
  171. industry: '家居定制',
  172. scenario: '线上获客',
  173. audience: '意向用户',
  174. evidence: ['第一次买不知道怎么选,怕踩雷。'],
  175. feedback: '老板不想做试用体验,更想先改评论区回复和案例展示'
  176. }
  177. });
  178. const deepDiveStructured = deepDiveResult.structuredContent || {};
  179. const deepDiveMessage = String(deepDiveStructured.assistantMessage || '');
  180. if (deepDiveStructured.status !== 'ok' ||
  181. !deepDiveMessage.includes('## 可执行解决动作') ||
  182. !deepDiveMessage.includes('## 优先级判断') ||
  183. !deepDiveStructured.summary?.memoryPath) {
  184. throw new Error(`Expected boss-oriented deep dive result, got: ${JSON.stringify(deepDiveStructured)}`);
  185. }
  186. if (!deepDiveStructured.data?.memory?.blockedActions?.includes('试用体验') ||
  187. !deepDiveStructured.data?.memory?.preferredActions?.includes('评论区回复')) {
  188. throw new Error('Expected deep dive MCP call to parse feedback into scoped memory');
  189. }
  190. if (!JSON.stringify(deepDiveStructured.nextActions || []).includes('live')) {
  191. throw new Error('Expected deep dive next actions to hand off to live collection instead of trapping the workflow');
  192. }
  193. const contentPlanResult = await client.callTool({
  194. name: 'voc_content_plan_run',
  195. arguments: {
  196. brand: 'MCP示例品牌',
  197. industry: '家居定制',
  198. issues: ['第一次买怕踩雷', '觉得价格贵', '怕质量不稳定']
  199. }
  200. });
  201. const contentPlanStructured = contentPlanResult.structuredContent || {};
  202. const contentPlanMessage = String(contentPlanStructured.assistantMessage || '');
  203. if (contentPlanStructured.status !== 'ok' ||
  204. !contentPlanMessage.includes('7 天 VOC 内容选题') ||
  205. !contentPlanMessage.includes('前 3 秒钩子') ||
  206. !contentPlanStructured.data?.plan?.length) {
  207. throw new Error(`Expected VOC content plan result, got: ${JSON.stringify(contentPlanStructured)}`);
  208. }
  209. const speakingScriptResult = await client.callTool({
  210. name: 'voc_speaking_script_run',
  211. arguments: {
  212. brand: 'MCP示例品牌',
  213. industry: '家居定制',
  214. topic: '第一次买怎么选不踩雷',
  215. userIssue: '第一次买怕选错',
  216. evidence: ['第一次买不知道怎么选,怕踩雷'],
  217. feedback: '开头更狠一点,老板视角,少讲概念,多给具体场景',
  218. memory: '../outputs/claude-code-xhs-mcp-smoke/voc-speaking-script-memory.json',
  219. finalize: true
  220. }
  221. });
  222. const speakingScriptStructured = speakingScriptResult.structuredContent || {};
  223. const speakingScriptMessage = String(speakingScriptStructured.assistantMessage || '');
  224. if (speakingScriptStructured.status !== 'ok' ||
  225. !speakingScriptMessage.includes('VOC 口播脚本共创') ||
  226. !speakingScriptMessage.includes('60 秒口播稿') ||
  227. !speakingScriptMessage.includes('30 秒压缩版') ||
  228. !speakingScriptStructured.data?.memory?.finalizedScripts?.length ||
  229. !speakingScriptStructured.data?.memory?.preferredStyles?.includes('老板视角')) {
  230. throw new Error(`Expected VOC speaking script result, got: ${JSON.stringify(speakingScriptStructured)}`);
  231. }
  232. const competitorMapResult = await client.callTool({
  233. name: 'voc_competitor_map_run',
  234. arguments: {
  235. brand: 'MCP示例品牌',
  236. city: '本地',
  237. category: '家居定制'
  238. }
  239. });
  240. const competitorMapStructured = competitorMapResult.structuredContent || {};
  241. const competitorMapMessage = String(competitorMapStructured.assistantMessage || '');
  242. if (competitorMapStructured.status !== 'ok' ||
  243. !competitorMapMessage.includes('竞品图谱') ||
  244. !competitorMapMessage.includes('错位竞争') ||
  245. !competitorMapStructured.data?.competitors?.length) {
  246. throw new Error(`Expected VOC competitor map result, got: ${JSON.stringify(competitorMapStructured)}`);
  247. }
  248. const businessWorkflowResult = await client.callTool({
  249. name: 'voc_business_workflow_run',
  250. arguments: {
  251. brand: 'MCP示例品牌',
  252. industry: '家居定制',
  253. platform: 'douyin',
  254. collectionMode: 'sample',
  255. keywords: ['家居定制怎么选', '第一次买怎么对比', '售后']
  256. }
  257. });
  258. const businessWorkflowStructured = businessWorkflowResult.structuredContent || {};
  259. const businessWorkflowMessage = String(businessWorkflowStructured.assistantMessage || '');
  260. if (businessWorkflowStructured.status !== 'ok' ||
  261. !businessWorkflowMessage.includes('VOC经营闭环') ||
  262. businessWorkflowStructured.summary?.workflowStage !== 'business_workflow_first_round') {
  263. throw new Error(`Expected VOC business workflow result, got: ${JSON.stringify(businessWorkflowStructured)}`);
  264. }
  265. for (const tool of ['voc_api_search', 'voc_api_doc', 'voc_api_call']) {
  266. if (!toolNames.includes(tool)) {
  267. throw new Error(`Expected catalog tool ${tool}, got: ${toolNames.join(', ')}`);
  268. }
  269. }
  270. const apiSearchResult = await client.callTool({
  271. name: 'voc_api_search',
  272. arguments: { platform: 'douyin' }
  273. });
  274. const apiSearchStructured = apiSearchResult.structuredContent || {};
  275. if (apiSearchStructured.status !== 'ok' || !apiSearchStructured.data?.endpoints?.length) {
  276. throw new Error(`Expected voc_api_search to list douyin endpoints, got: ${JSON.stringify(apiSearchStructured)}`);
  277. }
  278. const taobaoDetailSearchResult = await client.callTool({
  279. name: 'voc_api_search',
  280. arguments: { platform: 'taobao', query: '淘宝 商品详情' }
  281. });
  282. const taobaoDetailEndpoints = taobaoDetailSearchResult.structuredContent?.data?.endpoints || [];
  283. const taobaoDetailIds = taobaoDetailEndpoints.map(endpoint => endpoint.id);
  284. if (taobaoDetailIds[0] !== 'taobao.get_item_detail_v7') {
  285. throw new Error(`Expected current Taobao endpoints with v7 preferred for fresh product details, got: ${taobaoDetailIds.join(', ')}`);
  286. }
  287. const taobaoCatalogResult = await client.callTool({
  288. name: 'voc_api_search',
  289. arguments: { platform: 'taobao' }
  290. });
  291. const taobaoCatalogIds = (taobaoCatalogResult.structuredContent?.data?.endpoints || []).map(endpoint => endpoint.id);
  292. for (const id of ['taobao.get_item_detail_v3', 'taobao.get_item_detail_v6', 'taobao.get_item_detail_v7', 'taobao.get_shop_item_list_v4']) {
  293. if (!taobaoCatalogIds.includes(id)) {
  294. throw new Error(`Expected current Taobao catalog to include ${id}, got: ${taobaoCatalogIds.join(', ')}`);
  295. }
  296. }
  297. const taobaoSalesSearchResult = await client.callTool({
  298. name: 'voc_api_search',
  299. arguments: { platform: 'taobao', query: '淘宝 页面销量' }
  300. });
  301. const taobaoSalesEndpoints = taobaoSalesSearchResult.structuredContent?.data?.endpoints || [];
  302. if (taobaoSalesEndpoints[0]?.id !== 'taobao.get_item_detail_v3' || taobaoSalesEndpoints[0]?.deprecated) {
  303. throw new Error(`Expected Taobao v3 to be preferred for page sales, got: ${JSON.stringify(taobaoSalesEndpoints)}`);
  304. }
  305. const taobaoCouponPriceSearchResult = await client.callTool({
  306. name: 'voc_api_search',
  307. arguments: { platform: 'taobao', query: '淘宝 券后价' }
  308. });
  309. const taobaoCouponPriceEndpoints = taobaoCouponPriceSearchResult.structuredContent?.data?.endpoints || [];
  310. if (taobaoCouponPriceEndpoints[0]?.id !== 'taobao.get_item_detail_v4') {
  311. throw new Error(`Expected Taobao v4 to be preferred for coupon prices, got: ${JSON.stringify(taobaoCouponPriceEndpoints)}`);
  312. }
  313. const taobaoV9DocResult = await client.callTool({
  314. name: 'voc_api_doc',
  315. arguments: { id: 'taobao.get_item_detail_v9' }
  316. });
  317. const taobaoV9Endpoint = taobaoV9DocResult.structuredContent?.data?.endpoint || {};
  318. if (!String(taobaoV9Endpoint.responseHint || '').includes('totalCount=') ||
  319. Number(taobaoV9Endpoint.recommendationPriority) >= 0) {
  320. throw new Error(`Expected Taobao v9 field semantics and compatibility priority, got: ${JSON.stringify(taobaoV9Endpoint)}`);
  321. }
  322. const apiDocResult = await client.callTool({
  323. name: 'voc_api_doc',
  324. arguments: { id: 'douyin.search_general' }
  325. });
  326. const apiDocStructured = apiDocResult.structuredContent || {};
  327. if (apiDocStructured.status !== 'ok' ||
  328. !String(apiDocStructured.data?.endpoint?.proxyPath || '').includes('fetch_general_search_v2')) {
  329. throw new Error(`Expected voc_api_doc to return douyin search doc, got: ${JSON.stringify(apiDocStructured)}`);
  330. }
  331. const apiCallSentinel = 'sk-SMOKE_WRONG_TOKEN_TYPE';
  332. const apiCallResult = await client.callTool({
  333. name: 'voc_api_call',
  334. arguments: { id: 'douyin.search_general', params: { keyword: '律师' }, newapiToken: apiCallSentinel, vocToken: 'r:SMOKE_INVALID_SESSION_TOKEN' }
  335. });
  336. const apiCallStructured = apiCallResult.structuredContent || {};
  337. if (apiCallStructured.status !== 'needs_valid_token') {
  338. throw new Error(`Expected voc_api_call to map invalid sk-/r: tokens to needs_valid_token (token problem, not 余额不足), got: ${JSON.stringify(apiCallStructured)}`);
  339. }
  340. if (JSON.stringify(apiCallStructured).includes(apiCallSentinel) || JSON.stringify(apiCallStructured).includes('r:SMOKE_INVALID_SESSION_TOKEN')) {
  341. throw new Error('voc_api_call leaked the request-scoped token');
  342. }
  343. console.log(JSON.stringify({
  344. status: 'ok',
  345. tools: toolNames,
  346. assistantMessagePreview: assistantMessage.slice(0, 120),
  347. deepDivePreview: deepDiveMessage.slice(0, 120),
  348. contentPlanPreview: contentPlanMessage.slice(0, 120),
  349. speakingScriptPreview: speakingScriptMessage.slice(0, 120),
  350. competitorMapPreview: competitorMapMessage.slice(0, 120),
  351. businessWorkflowPreview: businessWorkflowMessage.slice(0, 120),
  352. preferenceMemoryApplied: Boolean(structured.summary && structured.summary.preferenceMemoryApplied),
  353. files: structured.files || []
  354. }, null, 2));
  355. } finally {
  356. await client.close();
  357. }
  358. }
  359. main().catch(error => {
  360. console.error(error);
  361. process.exit(1);
  362. });