# Directory Structure ``` ├── .gitignore ├── index.ts ├── LICENSE ├── package-lock.json ├── package.json ├── README.md └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | build 2 | node_modules 3 | npm-debug.log 4 | yarn-error.log 5 | yarn-debug.log 6 | yarn.lock 7 | .DS_Store 8 | .idea 9 | .vscode 10 | coverage 11 | dist 12 | *.log ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # <img src="https://cdn.worldvectorlogo.com/logos/tiktok-icon-2.svg" height="32"> TikTok MCP 2 | 3 |  4 | 5 | 6 | The TikTok MCP integrates TikTok access into Claude AI and other apps via TikNeuron. This TikTok MCP allows you to 7 | - analyze TikTok videos to determine virality factors 8 | - get content from TikTok videos 9 | - chat with TikTok videos 10 | 11 | ## Available Tools 12 | 13 | ### tiktok_get_subtitle 14 | 15 | **Description:** 16 | Get the subtitle (content) for a TikTok video url. This is used for getting the subtitle, content or context for a TikTok video. If no language code is provided, the tool will return the subtitle of automatic speech recognition. 17 | 18 | **Input Parameters:** 19 | - `tiktok_url` (required): TikTok video URL, e.g., https://www.tiktok.com/@username/video/1234567890 or https://vm.tiktok.com/1234567890 20 | - `language_code` (optional): Language code for the subtitle, e.g., en for English, es for Spanish, fr for French, etc. 21 | 22 | ### tiktok_get_post_details 23 | 24 | **Description:** 25 | Get the details of a TikTok post. Returns the details of the video like: 26 | - Description 27 | - Video ID 28 | - Creator username 29 | - Hashtags 30 | - Number of likes, shares, comments, views and bookmarks 31 | - Date of creation 32 | - Duration of the video 33 | - Available subtitles with language and source information 34 | 35 | **Input Parameters:** 36 | - `tiktok_url` (required): TikTok video URL, e.g., https://www.tiktok.com/@username/video/1234567890 or https://vm.tiktok.com/1234567890, or just the video ID like 7409731702890827041 37 | 38 | ### tiktok_search 39 | 40 | **Description:** 41 | Search for TikTok videos based on a query. Returns a list of videos matching the search criteria with their details including description, video ID, creator, hashtags, engagement metrics, date of creation, duration and available subtitles, plus pagination metadata for continuing the search. 42 | 43 | **Input Parameters:** 44 | - `query` (required): Search query for TikTok videos, e.g., 'funny cats', 'dance', 'cooking tutorial' 45 | - `cursor` (optional): Pagination cursor for getting more results 46 | - `search_uid` (optional): Search session identifier for pagination 47 | 48 | ## Requirements 49 | 50 | For this TikTok MCP, you need 51 | - NodeJS v18 or higher (https://nodejs.org/) 52 | - Git (https://git-scm.com/) 53 | - TikNeuron Account and MCP API Key (https://tikneuron.com/tools/tiktok-mcp) 54 | 55 | ## Setup 56 | 57 | 1. Clone the repository 58 | ``` 59 | git clone https://github.com/Seym0n/tiktok-mcp.git 60 | ``` 61 | 62 | 2. Install dependencies 63 | ``` 64 | npm install 65 | ``` 66 | 67 | 3. Build project 68 | ``` 69 | npm run build 70 | ``` 71 | 72 | This creates the file `build\index.js` 73 | 74 | ## Using in Claude AI 75 | 76 | Add the following entry to `mcpServers`: 77 | 78 | ``` 79 | "tiktok-mcp": { 80 | "command": "node", 81 | "args": [ 82 | "path\\build\\index.js" 83 | ], 84 | "env": { 85 | "TIKNEURON_MCP_API_KEY": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 86 | } 87 | } 88 | ``` 89 | 90 | and replace path with the `path` to TikTok MCP and `XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX` with TIkNeuron API Key 91 | 92 | so that `mcpServers` will look like this: 93 | 94 | ``` 95 | { 96 | "mcpServers": { 97 | "tiktok-mcp": { 98 | "command": "node", 99 | "args": [ 100 | "path\\build\\index.js" 101 | ], 102 | "env": { 103 | "TIKNEURON_MCP_API_KEY": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" 104 | } 105 | } 106 | } 107 | } 108 | ``` 109 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "resolveJsonModule": true, 11 | "outDir": "./build", 12 | "rootDir": "." 13 | }, 14 | "exclude": ["node_modules"] 15 | } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "tiktok-mcp", 3 | "version": "0.0.1", 4 | "description": "MCP server for TikTok", 5 | "license": "MIT", 6 | "author": "Simon", 7 | "homepage": "https://tikneuron.com", 8 | "type": "module", 9 | "bin": { 10 | "tiktok-mcp-server": "build/index.js" 11 | }, 12 | "files": [ 13 | "build" 14 | ], 15 | "scripts": { 16 | "build": "tsc && shx chmod +x build/*.js", 17 | "prepare": "npm run build", 18 | "watch": "tsc --watch" 19 | }, 20 | "dependencies": { 21 | "@modelcontextprotocol/sdk": "1.10.1" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^22", 25 | "shx": "^0.3.4", 26 | "typescript": "^5.6.2" 27 | } 28 | } ``` -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { 6 | CallToolRequestSchema, 7 | ListToolsRequestSchema, 8 | Tool, 9 | } from "@modelcontextprotocol/sdk/types.js"; 10 | 11 | 12 | const GET_SUBTITLE: Tool = { 13 | name: "tiktok_get_subtitle", 14 | description: 15 | "Get the subtitle (content) for a TikTok video url." + 16 | "This is used for getting the subtitle, content or context for a TikTok video." + 17 | "Supports TikTok video url (or video ID) as input and optionally language code from the tool post details" + 18 | "Returns the subtitle for the video in the requested language and format." + 19 | "If no language code is provided, the tool will return the subtitle of automatic speech recognition.", 20 | inputSchema: { 21 | type: "object", 22 | properties: { 23 | tiktok_url: { 24 | type: "string", 25 | description: "TikTok video URL, e.g., https://www.tiktok.com/@username/video/1234567890 or https://vm.tiktok.com/1234567890, or just the video ID like 7409731702890827041", 26 | }, 27 | language_code: { 28 | type: "string", 29 | description: "Language code for the subtitle, e.g., en for English, es for Spanish, fr for French, etc.", 30 | }, 31 | }, 32 | required: ["tiktok_url"] 33 | } 34 | }; 35 | 36 | const GET_POST_DETAILS: Tool = { 37 | name: "tiktok_get_post_details", 38 | description: 39 | "Get the details of a TikTok post." + 40 | "This is used for getting the details of a TikTok post." + 41 | "Supports TikTok video url (or video ID) as input." + 42 | "Returns the details of the video like" + 43 | " - Description" + 44 | " - Video ID" + 45 | " - Creator username" + 46 | " - Hashtags" + 47 | " - Number of likes, shares, comments, views and bookmarks" + 48 | " - Date of creation" + 49 | " - Duration of the video" + 50 | " - Available subtitles with language and source information", 51 | inputSchema: { 52 | type: "object", 53 | properties: { 54 | tiktok_url: { 55 | type: "string", 56 | description: "TikTok video URL, e.g., https://www.tiktok.com/@username/video/1234567890 or https://vm.tiktok.com/1234567890, or just the video ID like 7409731702890827041", 57 | }, 58 | }, 59 | required: ["tiktok_url"], 60 | }, 61 | }; 62 | 63 | const SEARCH: Tool = { 64 | name: "tiktok_search", 65 | description: 66 | "Search for TikTok videos based on a query." + 67 | "This is used for searching TikTok videos by keywords, hashtags, or other search terms." + 68 | "Supports search query as input and optional cursor and search_uid for pagination." + 69 | "Returns a list of videos matching the search criteria with their details including" + 70 | " - Description, video ID, creator, hashtags, engagement metrics, date of creation, duration of the video and available subtitles with language and source information" + 71 | " - Pagination metadata for continuing the search", 72 | inputSchema: { 73 | type: "object", 74 | properties: { 75 | query: { 76 | type: "string", 77 | description: "Search query for TikTok videos, e.g., 'funny cats', 'dance', 'cooking tutorial'", 78 | }, 79 | cursor: { 80 | type: "string", 81 | description: "Pagination cursor for getting more results (optional)", 82 | }, 83 | search_uid: { 84 | type: "string", 85 | description: "Search session identifier for pagination (optional)", 86 | }, 87 | }, 88 | required: ["query"], 89 | }, 90 | }; 91 | 92 | // Server implementation 93 | const server = new Server( 94 | { 95 | name: "tikneuron/tiktok-mcp", 96 | version: "0.1.0", 97 | }, 98 | { 99 | capabilities: { 100 | tools: {}, 101 | }, 102 | }, 103 | ); 104 | 105 | // Check for API key 106 | const TIKNEURON_MCP_API_KEY = process.env.TIKNEURON_MCP_API_KEY!; 107 | if (!TIKNEURON_MCP_API_KEY) { 108 | console.error("Error: TIKNEURON_MCP_API_KEY environment variable is required"); 109 | process.exit(1); 110 | } 111 | 112 | 113 | interface Subtitle { 114 | success?: boolean; 115 | subtitles?: Array<{ 116 | language?: string; 117 | source?: string; 118 | }>; 119 | subtitle_content?: string; 120 | } 121 | 122 | interface PostDetails { 123 | success: boolean; 124 | details: { 125 | description: string; 126 | video_id: string; 127 | creator: string; 128 | hashtags: string[]; 129 | likes: string; 130 | shares: string; 131 | comments: string; 132 | views: string; 133 | bookmarks: string; 134 | created_at: string; 135 | duration: number; 136 | available_subtitles: Array<{ 137 | language?: string; 138 | source?: string; 139 | }>; 140 | }; 141 | } 142 | 143 | interface SearchResult { 144 | success: boolean; 145 | videos: Array<{ 146 | description: string; 147 | video_id: string; 148 | creator: string; 149 | hashtags: string[]; 150 | likes: string; 151 | shares: string; 152 | comments: string; 153 | views: string; 154 | bookmarks: string; 155 | created_at: string; 156 | duration: number; 157 | available_subtitles: Array<{ 158 | language?: string; 159 | source?: string; 160 | }>; 161 | }>; 162 | metadata: { 163 | cursor: string; 164 | has_more: boolean; 165 | search_uid: string; 166 | }; 167 | } 168 | 169 | 170 | 171 | function isGetSubtitleArgs(args: unknown): args is { tiktok_url: string, language_code: string } { 172 | return ( 173 | typeof args === "object" && 174 | args !== null && 175 | "tiktok_url" in args && 176 | typeof (args as { tiktok_url: string }).tiktok_url === "string" 177 | ); 178 | } 179 | 180 | function isGetPostDetailsArgs(args: unknown): args is { tiktok_url: string } { 181 | return ( 182 | typeof args === "object" && 183 | args !== null && 184 | "tiktok_url" in args && 185 | typeof (args as { tiktok_url: string }).tiktok_url === "string" 186 | ); 187 | } 188 | 189 | function isSearchArgs(args: unknown): args is { query: string, cursor?: string, search_uid?: string } { 190 | return ( 191 | typeof args === "object" && 192 | args !== null && 193 | "query" in args && 194 | typeof (args as { query: string }).query === "string" && 195 | ("cursor" in args ? typeof (args as { cursor?: string }).cursor === "string" : true) && 196 | ("search_uid" in args ? typeof (args as { search_uid?: string }).search_uid === "string" : true) 197 | ); 198 | } 199 | 200 | async function performSearch(query: string, cursor?: string, search_uid?: string) { 201 | const url = new URL('https://tikneuron.com/api/mcp/search'); 202 | url.searchParams.set('query', query); 203 | 204 | if (cursor) { 205 | url.searchParams.set('cursor', cursor); 206 | } 207 | 208 | if (search_uid) { 209 | url.searchParams.set('search_uid', search_uid); 210 | } 211 | 212 | const response = await fetch(url, { 213 | headers: { 214 | 'Accept': 'application/json', 215 | 'Accept-Encoding': 'gzip', 216 | 'MCP-API-KEY': TIKNEURON_MCP_API_KEY, 217 | } 218 | }); 219 | 220 | if (!response.ok) { 221 | throw new Error(`TikNeuron API error: ${response.status} ${response.statusText}\n${await response.text()}`); 222 | } 223 | 224 | const data = await response.json() as SearchResult; 225 | 226 | if (data.videos && data.videos.length > 0) { 227 | const videosList = data.videos.map((video, index) => { 228 | return `Video ${index + 1}: 229 | Description: ${video.description || 'N/A'} 230 | Video ID: ${video.video_id || 'N/A'} 231 | Creator: ${video.creator || 'N/A'} 232 | Hashtags: ${Array.isArray(video.hashtags) ? video.hashtags.join(', ') : 'N/A'} 233 | Likes: ${video.likes || '0'} 234 | Shares: ${video.shares || '0'} 235 | Comments: ${video.comments || '0'} 236 | Views: ${video.views || '0'} 237 | Bookmarks: ${video.bookmarks || '0'} 238 | Created at: ${video.created_at || 'N/A'} 239 | Duration: ${video.duration || 0} seconds 240 | Available subtitles: ${video.available_subtitles?.map(sub => `${sub.language || 'Unknown'} (${sub.source || 'Unknown source'})`).join(', ') || 'None'}`; 241 | }).join('\n\n'); 242 | 243 | const metadata = `\nSearch Metadata: 244 | Cursor: ${data.metadata?.cursor || 'N/A'} 245 | Has more results: ${data.metadata?.has_more ? 'Yes' : 'No'} 246 | Search UID: ${data.metadata?.search_uid || 'N/A'}`; 247 | 248 | return videosList + metadata; 249 | } else { 250 | return 'No videos found for the search query'; 251 | } 252 | } 253 | 254 | async function performGetSubtitle(tiktok_url: string, language_code: string) { 255 | const url = new URL('https://tikneuron.com/api/mcp/get-subtitles'); 256 | url.searchParams.set('tiktok_url', tiktok_url); 257 | 258 | if (language_code){ 259 | url.searchParams.set('language_code', language_code); 260 | } 261 | 262 | const response = await fetch(url, { 263 | headers: { 264 | 'Accept': 'application/json', 265 | 'Accept-Encoding': 'gzip', 266 | 'MCP-API-KEY': TIKNEURON_MCP_API_KEY, 267 | } 268 | }); 269 | 270 | if (!response.ok) { 271 | throw new Error(`TikNeuron API error: ${response.status} ${response.statusText}\n${await response.text()}`); 272 | } 273 | 274 | const data = await response.json() as Subtitle; 275 | 276 | return data.subtitle_content || 'No subtitle available'; 277 | } 278 | 279 | async function performGetPostDetails(tiktok_url: string) { 280 | const url = new URL('https://tikneuron.com/api/mcp/post-detail'); 281 | url.searchParams.set('tiktok_url', tiktok_url); 282 | 283 | const response = await fetch(url, { 284 | headers: { 285 | 'Accept': 'application/json', 286 | 'Accept-Encoding': 'gzip', 287 | 'MCP-API-KEY': TIKNEURON_MCP_API_KEY, 288 | } 289 | }); 290 | 291 | if (!response.ok) { 292 | throw new Error(`TikNeuron API error: ${response.status} ${response.statusText}\n${await response.text()}`); 293 | } 294 | 295 | const data = await response.json() as PostDetails; 296 | 297 | if (data.details) { 298 | const details = data.details; 299 | return `Description: ${details.description || 'N/A'} 300 | Video ID: ${details.video_id || 'N/A'} 301 | Creator: ${details.creator || 'N/A'} 302 | Hashtags: ${Array.isArray(details.hashtags) ? details.hashtags.join(', ') : 'N/A'} 303 | Likes: ${details.likes || '0'} 304 | Shares: ${details.shares || '0'} 305 | Comments: ${details.comments || '0'} 306 | Views: ${details.views || '0'} 307 | Bookmarks: ${details.bookmarks || '0'} 308 | Created at: ${details.created_at || 'N/A'} 309 | Duration: ${details.duration || 0} seconds 310 | Available subtitles: ${details.available_subtitles?.map(sub => `${sub.language || 'Unknown'} (${sub.source || 'Unknown source'})`).join(', ') || 'None'}`; 311 | } else { 312 | return 'No details available'; 313 | } 314 | } 315 | 316 | 317 | // Tool handlers 318 | server.setRequestHandler(ListToolsRequestSchema, async () => ({ 319 | tools: [GET_SUBTITLE, GET_POST_DETAILS, SEARCH], 320 | })); 321 | 322 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 323 | try { 324 | const { name, arguments: args } = request.params; 325 | 326 | if (!args) { 327 | throw new Error("No arguments provided"); 328 | } 329 | 330 | switch (name) { 331 | case "tiktok_get_subtitle": { 332 | if (!isGetSubtitleArgs(args)) { 333 | throw new Error("Invalid arguments for tiktok_get_subtitle"); 334 | } 335 | const { tiktok_url, language_code } = args; 336 | 337 | const results = await performGetSubtitle(tiktok_url, language_code); 338 | return { 339 | content: [{ type: "text", text: results }], 340 | isError: false, 341 | }; 342 | } 343 | 344 | case "tiktok_get_post_details": { 345 | if (!isGetPostDetailsArgs(args)) { 346 | throw new Error("Invalid arguments for tiktok_get_post_details"); 347 | } 348 | const { tiktok_url } = args; 349 | 350 | const results = await performGetPostDetails(tiktok_url); 351 | return { 352 | content: [{ type: "text", text: results }], 353 | isError: false, 354 | }; 355 | } 356 | 357 | case "tiktok_search": { 358 | if (!isSearchArgs(args)) { 359 | throw new Error("Invalid arguments for tiktok_search"); 360 | } 361 | const { query, cursor, search_uid } = args; 362 | 363 | const results = await performSearch(query, cursor, search_uid); 364 | return { 365 | content: [{ type: "text", text: results }], 366 | isError: false, 367 | }; 368 | } 369 | 370 | default: 371 | return { 372 | content: [{ type: "text", text: `Unknown tool: ${name}` }], 373 | isError: true, 374 | }; 375 | } 376 | } catch (error) { 377 | return { 378 | content: [ 379 | { 380 | type: "text", 381 | text: `Error: ${error instanceof Error ? error.message : String(error)}`, 382 | }, 383 | ], 384 | isError: true, 385 | }; 386 | } 387 | }); 388 | 389 | async function runServer() { 390 | const transport = new StdioServerTransport(); 391 | await server.connect(transport); 392 | console.error("TikTok MCP Server running on stdio"); 393 | } 394 | 395 | runServer().catch((error) => { 396 | console.error("Fatal error running server:", error); 397 | process.exit(1); 398 | }); ```