tiktok-scraper-mcp 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.js +6 -6
- package/build/scraper.d.ts +1 -1
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// tiktok-scraper-mcp v1.0.0 | Benetti Corporation | All rights reserved
|
|
3
|
-
import{McpServer as
|
|
3
|
+
import{McpServer as re}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as se}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as M}from"zod";import H from"puppeteer-extra";import ee from"puppeteer-extra-plugin-stealth";H.use(ee());var w={commentItem:'[class*="DivCommentItemWrapper"]',usernameContent:'[class*="DivUsernameContentWrapper"]',commentContent:'[class*="DivCommentContentWrapper"]',commentHeader:'[class*="DivCommentHeaderWrapper"]',commentSubContent:'[class*="DivCommentSubContentSplitWrapper"]',subContentWrapper:'[class*="DivCommentSubContentWrapper"]',likeContainer:'[class*="DivLikeContainer"]',replyContainer:'[class*="DivReplyContainer"]',viewReplies:'[class*="DivViewRepliesContainer"]',viewMoreReplies:'[class*="DivViewMoreRepliesOptionsContainer"]'};function b(e){return new Promise(o=>setTimeout(o,e))}async function V(e){let{url:o,scrollTimeoutSec:s=30,maxReplyRounds:i=20,headless:R=!0}=e,O=o.match(/\/video\/(\d+)/),z=O?O[1]:null,C=null;try{C=await H.launch({headless:R,args:["--no-sandbox","--disable-setuid-sandbox"]});let a=await C.newPage();await a.setViewport({width:1280,height:900}),await a.setUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36");let v=!0,L=0;a.on("response",async t=>{try{let n=t.url();if(!n.includes("/comment/list")&&!n.includes("comment_list")||t.status()!==200)return;let r=await t.text(),m=JSON.parse(r),g=m.comments||[];L+=g.length,v=m.has_more===1||m.has_more===!0,console.error(` API batch: +${g.length} comments (total: ${L}, has_more: ${v})`)}catch{}}),console.error(`Navigating to ${o}...`),await a.goto(o,{waitUntil:"networkidle2",timeout:3e4}),console.error("Muting video...");try{await a.waitForSelector("video",{timeout:5e3}),await a.evaluate(()=>{document.querySelectorAll("video, audio").forEach(t=>{t.muted=!0,t.volume=0,t.pause()})}),await a.keyboard.press("m")}catch{console.error("No video element found to mute")}console.error("Looking for Comments tab...");try{await a.waitForSelector('span[data-testid="tux-web-text"]',{timeout:1e4});let t=await a.$$('span[data-testid="tux-web-text"]');for(let n of t){let r=await a.evaluate(m=>m.textContent,n);if(r&&r.trim().toLowerCase().startsWith("comment")){await n.click(),console.error("Clicked Comments tab"),await b(2e3);break}}}catch{console.error("No Comments tab found, comments may already be visible")}console.error("Waiting for comments to appear...");try{await a.waitForSelector(w.commentItem,{timeout:15e3})}catch{throw new Error("No comments found. The video may not have comments, or TikTok blocked the request.")}let N=await a.evaluate(t=>{let n=document.querySelector(t);if(!n)return null;let r=n.parentElement;for(;r&&r!==document.body;){let m=window.getComputedStyle(r);if(m.overflowY==="scroll"||m.overflowY==="auto")return r.id=r.id||"__tss_scroll_container","#"+r.id;r=r.parentElement}return null},w.commentItem);console.error("Loading all comments...");let S=0,q=Date.now(),X=s*1e3;for(;v&&Date.now()-q<X;)try{N?await a.evaluate(n=>{let r=document.querySelector(n);r&&(r.scrollTop=r.scrollHeight)},N):await a.evaluate(()=>window.scrollTo(0,document.body.scrollHeight)),await b(800);let t=await a.$$eval(w.commentItem,n=>n.length);t>S&&(q=Date.now(),S=t,console.error(` ${t} comments in DOM...`))}catch(t){if(t.message?.includes("detached")||t.message?.includes("Execution context")){console.error(" Frame detached, waiting..."),await b(3e3);continue}throw t}console.error(v?`Scroll timeout reached: ${S} in DOM`:`All comments loaded (API confirmed): ${S} in DOM`);let A=await a.$$eval(w.viewReplies,t=>t.length);console.error(`Expanding ${A} reply threads...`);for(let t=0;t<A;t++)await a.$$eval(w.viewReplies,(n,r)=>{n[r]&&n[r].click()},t),await b(350);await b(1500),console.error("Loading more replies...");let T=0,x=!0,F=new Set;for(;x&&T<i;){x=!1;let t=await a.$$(w.viewMoreReplies);for(let n=0;n<t.length;n++)F.has(n)||(F.add(n),await t[n].click(),x=!0,await b(600));x&&(T++,console.error(` Reply round ${T}...`),await b(1500))}console.error("Extracting comment data...");let y=await a.evaluate(t=>{function n(u){let f=u.querySelector(t.commentContent);if(!f)return null;let c=f.querySelectorAll("p, span");for(let p=0;p<c.length;p++){let h=c[p];if(!h.closest(t.commentHeader)&&!h.closest(t.commentSubContent)&&h.textContent?.trim())return h.textContent.trim()}return null}function r(u){let f=u.querySelector(t.usernameContent),c=u.querySelector(t.likeContainer),p=u.querySelector(t.subContentWrapper),h=p&&p.textContent?.trim().replace(/Reply$/i,"").trim()||null;return{username:f&&f.textContent?.trim()||null,text:n(u),likes:c&&c.textContent?.trim()||null,time:h}}let m=document.querySelectorAll(t.commentItem),g=[],W=0;return m.forEach((u,f)=>{let c=r(u);c.index=f+1,c.replies=[];let p=u.querySelector(t.replyContainer);p&&(p.querySelectorAll(t.commentItem).forEach((Q,Z)=>{let P=r(Q);P.index=Z+1,c.replies.push(P)}),W+=c.replies.length),(c.text||c.username)&&g.push(c)}),{comments:g,totalReplies:W}},w);return console.error(`Done: ${y.comments.length} comments, ${y.totalReplies} replies`),{videoId:z,url:o,commentCount:y.comments.length,replyCount:y.totalReplies,comments:y.comments}}finally{C&&await C.close()}}import*as d from"fs";import*as U from"path";import*as l from"os";import*as Y from"crypto";var E=U.join(l.homedir(),".tiktok-scraper-mcp"),I=U.join(E,"config.json"),te="https://us-central1-benetti-417611.cloudfunctions.net/check-subscription",oe="tiktok-scraper",j=3;function k(){try{if(d.existsSync(I))return JSON.parse(d.readFileSync(I,"utf8"))}catch{}return{email:"",subscribed:!1,checkedAt:0,uses:0}}function D(e){d.existsSync(E)||d.mkdirSync(E,{recursive:!0}),d.writeFileSync(I,JSON.stringify(e,null,2))}function ne(){let e=`${l.hostname()}|${l.platform()}|${l.userInfo().username}|${l.arch()}`,o=Y.createHash("sha256").update(e).digest("hex").substring(0,12),s=`${l.hostname()} (${l.platform()})`;return{id:o,name:s}}async function J(e){let o=ne(),s=`${te}?email=${encodeURIComponent(e)}&product=${encodeURIComponent(oe)}&device_id=${encodeURIComponent(o.id)}&device_name=${encodeURIComponent(o.name)}`,i=await fetch(s);if(!i.ok)throw new Error(`Subscription check failed: HTTP ${i.status}`);return i.json()}async function B(e){let o=e.trim().toLowerCase();if(!o||!o.includes("@"))return"Invalid email address.";let s=k(),i=await J(o);return s.email=o,s.subscribed=i.subscribed,s.checkedAt=Date.now(),D(s),i.deviceLimitReached?`Device limit reached (${i.maxDevices} devices). Remove a device from another installation, or purchase an additional subscription at https://buy.stripe.com/4gM00l1OC52Tc7V7YN6EU00`:i.subscribed?`PRO subscription verified for ${o}. Unlimited scraping enabled.`:`Email saved. No active subscription found \u2014 ${j-s.uses} free uses remaining. Subscribe at https://buy.stripe.com/4gM00l1OC52Tc7V7YN6EU00`}async function _(){let e=k();if(e.email&&Date.now()-e.checkedAt>1440*60*1e3)try{let s=await J(e.email);e.subscribed=s.subscribed,e.checkedAt=Date.now(),D(e)}catch{}let o=Math.max(0,j-e.uses);return{email:e.email,subscribed:e.subscribed,uses:e.uses,usesLeft:o,canScrape:e.subscribed||o>0}}function G(){let e=k();e.uses++,D(e)}function K(){return!!k().email}var $=new re({name:"tiktok-scraper-mcp",version:"1.0.0"});$.registerTool("scrape_tiktok_comments",{description:"Scrape all comments and replies from a TikTok video URL. Returns structured JSON with usernames, comment text, likes, timestamps, and nested replies. Launches a local Puppeteer browser to scrape \u2014 no data leaves the user's machine. Requires a subscription email to be set first (use set_email tool).",inputSchema:{url:M.string().url().describe("TikTok video URL (e.g. https://www.tiktok.com/@user/video/1234567890)"),headless:M.boolean().optional().describe("Run browser in headless mode (default: true)")}},async({url:e,headless:o})=>{if(!e.includes("tiktok.com"))return{content:[{type:"text",text:"Error: URL must be a TikTok video URL."}]};let s=await _();if(!K())return{content:[{type:"text",text:`No email configured. Use the set_email tool first to set your subscription email.
|
|
4
4
|
|
|
5
|
-
${
|
|
5
|
+
${s.usesLeft} free scrapes available. Subscribe for unlimited access at https://buy.stripe.com/4gM00l1OC52Tc7V7YN6EU00`}]};if(!s.canScrape)return{content:[{type:"text",text:`Free tier exhausted (${s.uses} uses). Subscribe for unlimited access at https://buy.stripe.com/4gM00l1OC52Tc7V7YN6EU00
|
|
6
6
|
|
|
7
|
-
Use set_email with your subscription email after purchasing.`}]};try{let
|
|
8
|
-
${
|
|
9
|
-
`)},{type:"text",text:JSON.stringify(
|
|
10
|
-
`)}]}});async function
|
|
7
|
+
Use set_email with your subscription email after purchasing.`}]};try{let i=await V({url:e,headless:o??!0});return G(),{content:[{type:"text",text:[`Scraped ${i.commentCount} comments and ${i.replyCount} replies`,i.videoId?`Video ID: ${i.videoId}`:"",s.subscribed?"":`
|
|
8
|
+
${s.usesLeft-1} free scrapes remaining.`].filter(Boolean).join(`
|
|
9
|
+
`)},{type:"text",text:JSON.stringify(i,null,2)}]}}catch(i){return{content:[{type:"text",text:`Scrape failed: ${i instanceof Error?i.message:String(i)}`}]}}});$.registerTool("set_email",{description:"Set or update the subscription email for the TikTok scraper. Checks with Stripe to verify subscription status. Required before scraping. 3 free scrapes included without a subscription.",inputSchema:{email:M.string().email().describe("Your email address used for the subscription")}},async({email:e})=>{try{return{content:[{type:"text",text:await B(e)}]}}catch(o){return{content:[{type:"text",text:`Error: ${o instanceof Error?o.message:String(o)}`}]}}});$.registerTool("subscription_status",{description:"Check your current TikTok scraper subscription status, usage count, and remaining free scrapes.",inputSchema:{}},async()=>{let e=await _();return{content:[{type:"text",text:[`Email: ${e.email||"(not set)"}`,`Status: ${e.subscribed?"PRO \u2014 Unlimited":"Free Tier"}`,`Uses: ${e.uses}`,e.subscribed?"":`Free scrapes remaining: ${e.usesLeft}`,"",e.subscribed?"Manage subscription: https://us-central1-benetti-417611.cloudfunctions.net/customer-portal?email="+encodeURIComponent(e.email):"Subscribe for unlimited access: https://buy.stripe.com/4gM00l1OC52Tc7V7YN6EU00"].filter(Boolean).join(`
|
|
10
|
+
`)}]}});async function ie(){let e=new se;await $.connect(e),console.error("TikTok Scraper MCP server running on stdio")}ie().catch(e=>{console.error("Fatal error:",e),process.exit(1)});
|
package/build/scraper.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "tiktok-scraper-mcp",
|
|
3
3
|
"author": "Benetti Corporation",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.2",
|
|
6
6
|
"description": "MCP server that scrapes all TikTok comments and replies from any video. Runs locally via Puppeteer — no data leaves your machine.",
|
|
7
7
|
"homepage": "https://github.com/benetti-corporation/tiktok-scraper-mcp",
|
|
8
8
|
"repository": {
|