I ship a free TikTok creator toolkit at tiktapdown.com — 11 web tools: watermark-free downloader, TikTok-to-MP3, trending videos and hashtags for 16 countries, best time to post, hook library, hashtag finder, RPM and engagement calculators, Unicode fonts, keyword research. Ten of them are wrapped as an MCP server — tiktapdown-mcp — published on npm, the Anthropic MCP Registry and Glama, listed in awesome-mcp-servers.
It was actually the first of the three Tapdown servers I built (April 2026), before the X one and the Instagram one, but it's the one I never wrote up. Sitting down to do that is how I found out one of its tools had been silently broken for a while. More on that below — it's the most useful part of this post.
The whole server is one file, ~880 lines of TypeScript. Same shape as the other two.
The stack
-
TypeScript +
@modelcontextprotocol/sdk -
stdio transport for Claude Desktop / Cursor / Cline (
npx -y tiktapdown-mcp) -
Streamable HTTP transport when
PORTis set — an Express app with one/mcproute, so the same build runs on Railway or in Docker - zod schemas as the input contract
- No auth, no database, no per-user state
A tool is 20–60 lines:
server.tool(
"get_best_time_to_post",
"Best posting windows for a country, with reasoning.",
{ country: z.enum(["US","GB","DE", /* … */]) },
async ({ country }) => ({
content: [{ type: "text", text: renderSchedule(country) }],
}),
);
Ten of these. Three call out to the network (download, MP3, trends); the other seven are pure functions over data baked into the file — hashtag sets per niche, posting windows per country, hook formulas, RPM tables. Those never fail and never get rate-limited, which matters more than it sounds when an LLM is the caller.
TikTok has no syndication endpoint
The X post's big unlock was cdn.syndication.twimg.com — a public, stable JSON endpoint that returns full tweet data including media variants. I went looking for the TikTok equivalent.
There isn't one. TikTok does have an oEmbed endpoint:
https://www.tiktok.com/oembed?url=https://www.tiktok.com/@user/video/123
It returns the title, author, a thumbnail and an <iframe> embed. No media URL. That's it.
So download_tiktok_video and extract_tiktok_audio_mp3 go through a third-party resolver instead. The one I use (tikwm.com) takes a POST with the video URL and hd=1, and returns the no-watermark MP4, the watermarked one, the cover, the stats and the music info in one call. I've been running it in production on the web app for months.
Three things I learned about it that aren't in any docs:
- The media URLs are signed and short-lived, and they differ on every request for the same video. Don't cache the URL. Cache the metadata if you want, but hand the user a fresh link every time.
-
Anything under
music/*returns 403 from a server. The MP3 tool works because it goes through the same video resolver and pulls the audio track from that response, not from a music endpoint. -
It is not an official API. It could change or disappear. Keep the tool thin, keep the error message honest (
❌ Could not process video: …), and keep a link back to the web downloader in the output so the user has a fallback that doesn't depend on the MCP.
That last point is a pattern I ended up using in every tool.
Every tool output ends with a link
Look at what download_tiktok_video actually returns:
✅ TikTok Video Downloaded via TikTapDown
📌 Title: …
👤 Author: @…
📊 Stats: views / likes / comments / shares
🎵 Music: … by …
⬇️ Download Links:
• No Watermark (HD): https://…
• With Watermark: https://…
🔗 Full toolkit: https://tiktapdown.com
Plain text, emoji headings, deep link at the end. Not JSON.
I did this on purpose. When a tool returns JSON, the model has to re-explain it to the user and usually drops half the fields. When it returns prose, the model quotes it. And the trailing link is the whole business model — the MCP is free, the site is free, but the site is where the trends pages, the blog and the rest of the toolkit live. Every tool links to its own page (/hashtags/{niche}, /best-time/{country}, /trends/{country}, /rpm-calculator…), and the downloader pages accept ?url= so an assistant can send someone to a result that's already resolved.
Whether that link gets clicked is a different question — I'll come back to it.
The bug I found while writing this
get_tiktok_trends_by_country is supposed to be the best tool in the set: today's trending videos and hashtags for a country, pulled from the same daily pipeline that feeds tiktapdown.com/trends.
While pulling the source up for this post I actually called it. It returned this:
📈 TikTok Trends — US
Live trends data is best viewed on the dashboard…
🔗 https://tiktapdown.com/trends/US
That's the fallback — the text it returns when the fetch fails. The tool was calling tiktapdown.com/api/trends/US. The web app's route is /api/trends?country=US&category=videos. At some point the site's API moved, the MCP kept calling the old path, got a 404, swallowed it in a catch {} and served the fallback. No error, no log, no failed test. Nobody complained because the output looked fine.
Two things went wrong, and both are worth naming:
Wrapping your own site's API is not safer than wrapping someone else's. I treated tiktapdown.com/api/* as stable because I own it. But I ship the web app several times a week and the MCP a few times a year. The contract drifted and nothing was watching. The tikwm dependency — the "risky" third-party one — never broke. My own did.
A graceful fallback that hides failure is worse than a loud error. The fallback exists so the tool degrades nicely if the trends cache is empty for a country. It degraded so nicely I didn't notice for months.
The fix (v1.2.0) is boring: call the right route, and add a contract test that hits production — not a mock — once a day and fails if any tool that touches the site returns its fallback. The lesson from the X post ("ship the CI test harness on day one") wasn't enough. The harness tested the server against my assumptions. It needed to test against the site.
There was a second drift in the same tool: its country enum listed IN and ID, which the site doesn't cover, and was missing NL and SE, which it does. Same root cause — a list copied by hand into a second repo. The list now matches the site's, and the daily contract test diffs the two.
Where the trends data comes from
Since the trends tool is the one people ask about: the pipeline behind it changed completely this summer.
It started on Apify. A hashtag-scraper actor, ~$1.78 per run, and the free tier covers about three runs a month. "Top" results were often videos from 2021. When I dropped to the free plan the whole trends section quietly froze on stale data — the same silent-failure pattern as above, one layer down.
It now runs as a small Python job on a cheap VPS, once a day, at $0 in API cost:
- Videos: TikTok's regional feed plus a few seed hashtags per country, filtered by region, ranked inside age tiers (≤14 days first, then ≤60) so it surfaces what's climbing rather than what's evergreen-viral; max two videos per creator.
- Hashtags: TikTok's own Creative Center trend rankings for 14 of the 16 countries. Two (NL, SE) aren't supported there, so they fall back to a ranking derived from the video set — and the page says so.
The MCP tool doesn't know any of this. It gets the same JSON the site gets. Which is exactly why the contract test matters.
Publishing — what's different for the third server
I wrote up the publishing pipeline in detail in the X post (npm → Anthropic Registry → Glama → awesome-mcp-servers → the auto-synced directories) and the Instagram post (everything that changed in between). I won't repeat it. Two things specific to this one:
-
The registry does not follow npm. While npm was at
tiktapdown-mcp@1.1.0, the Anthropic Registry still listed1.0.1. Publishing to npm does not republish to the registry — you runmcp-publisher publishagain, every release, and it needs aserver.jsonin the repo. This repo didn't have one (the 1.0.1 publish was done from a scratch file), so the release after it silently never reached the registry. Same failure shape as the trends bug: a step with no error and nothing watching. -
glama.jsonhere is two lines — justmaintainers— because there's no pnpm lockfile to confuse their builder. The Instagram post covers the case where you need the build override.
For scale, the three servers combined do a few hundred npm downloads a month (last 30 days: tiktapdown-mcp 214, instapdown-mcp 208, xtapdown-mcp 176). Not big. But the web app's traffic is now majority AI-referred — over three quarters of sessions come from chatgpt.com — and I can't cleanly separate how much of that the MCP servers and their registry listings earned versus the site's own content. That's the honest answer to "does the trailing link get clicked": I don't know, and it's the next thing I'm instrumenting.
What I'd do differently
Test against production, on a schedule. Not in CI on push — the MCP repo doesn't change when the site does. A daily job that calls every network-backed tool for real and alerts on fallback output. This would have caught the trends bug in one day instead of months.
One source of truth for shared lists. Country codes, niche names, tool URLs — anything that exists in both the site and the MCP should be fetched from the site or generated from one file. Hand-copied enums drift.
Return prose, link back, but measure it. The text-with-deep-link output is right. Shipping it without a way to attribute the resulting traffic was not.
Keep the pure tools pure. Seven of ten tools have no network dependency. They're the ones that have never broken and the ones that make the server feel instant. If a tool can be a lookup table, make it a lookup table.
Try it
-
Install:
npx -y tiktapdown-mcp - Claude Desktop config:
{ "mcpServers": { "tiktapdown": { "command": "npx", "args": ["-y", "tiktapdown-mcp"] } } }
- Source: github.com/farukkolip/tiktapdown-mcp
- npm: tiktapdown-mcp
- The web tools it wraps: tiktapdown.com — free, no signup
- Siblings: xtapdown-mcp (X), instapdown-mcp (Instagram)
If you've solved the "test an MCP server against a moving upstream" problem in a nicer way than a daily cron, I'd genuinely like to hear it in the comments.











