Overview
Back in 2015 I was off on my first Europe adventure. During that first summer I was lucky enough to visit one of the best festivals in the world, in my favourite city in the world, Dekmantel Amsterdam. It was 3 days of awesomeness that I'll never forget. The people, the music and the experience were all just 10/10, and I even got lucky with the weather and had nice sunny days. Music has always been one of my main passions, and since then I've always remained on their mailing list. Each year when the lineup drops and the timetable is announced I'm always a bit jealous and wish I could be there. This year when I saw the timetable email, I had an idea. What if I built my own MCP to turn these images into a proper festival SoundCloud playlist?
So that's what I did! I'm deep in AI coding techniques and methods and use them daily at work, and I'm always watching YouTube videos from the leading voices on the best Claude Code methods and AI architecting approaches. So having that in mind, and my desire to learn more about MCPs, it was all I needed to dive into it. What I ended up with is a public 105 track playlist with one best mix per artist across the whole Dekmantel 2026 lineup, and honestly it's been such a good way to get into this year's names. Underneath it is a Model Context Protocol (MCP) server running on Cloudflare's edge that an assistant like Claude can drive to search SoundCloud, sort out the artists, rank their mixes and build the playlist for me.
What is MCP, and why build one?
If you haven't come across it before, Model Context Protocol is an open standard for handing an AI assistant a set of tools it can use. The easiest way to picture it is a typed menu of actions the model can read and call directly. Instead of writing a one off script that only ever does one job, I wrote a server that hands SoundCloud over to the model as a toolbox. Things like search_tracks, resolve_artist, find_mixes, best_mix_per_artist and create_playlist. Once it's connected the model can work out the plan and run the whole job itself, and I get to reuse the same toolbox for anything else I want to do on my account down the track.
It ended up being 27 tools all up, grouped into three lots. The discovery tools for searching and resolving tracks, users and playlists. The DJ mix smarts, which is the artist resolution and mix ranking logic that makes the festival idea actually work. And the engagement tools for the writes, so liking, reposting, following, commenting and the full playlist CRUD.
How it works
The server itself is a Cloudflare Worker, and if you peel it back it's really just three layers wrapped around that list of tools.
- An OAuth provider sitting at the edge that handles the connection coming in from the AI client and sends the traffic where it needs to go. Anything hitting
/mcpgoes to the agent and everything else goes to the auth routes. - A per user MCP agent built as a Cloudflare Durable Object. It holds the actual MCP server instance and is tied to one logged in SoundCloud user, and it speaks the Streamable HTTP transport that remote AI connectors expect.
- A tool registry where each tool is just a plain object with a name, a description, a Zod schema (which the SDK turns into the JSON Schema the model reads) and a handler. One loop registers all 27 of them, and every handler is thin glue sitting over a typed
SoundCloudClient, so none of the actual logic lives in the tools themselves.
The bit that trips people up is that there are two separate OAuth flows going on at once. First the AI client logs in against my server, so in that handshake my server is the one minting the token the assistant carries around. Then my server logs in against SoundCloud, so now my server is the client. Connecting the tool fires both of these back to back, and the user's SoundCloud tokens get stored encrypted in Workers KV and refreshed whenever they're needed.
The actual hard part: turning a flyer into the right artists
A festival lineup is a nightmare for a computer to read. The bill doesn't say "Shed", it says "Shed presents Rave Echoes". It says "Ben UFO & Call Super & Objekt & Pariah", and "DJ Sprinkles' Deeperama", and "Sass (Moxie & Peach & Saoirse & Shanti Celeste)". Before you can go and find anyone's best mix you first have to work out what each of these billings even means.
So the first pass is a big dedup with some clear rules. Split the back to back and collective billings out into individual artists, treat "X presents Y" as just the headliner X, and drop anything that isn't a DJ at all like the live bands, the panel talks and the sound systems. That got the roughly 150 raw billings down to 164 unique artists.
Then every one of those names has to become an actual SoundCloud profile. The resolver scores each candidate on a handful of signals like a permalink guess, an exact name match and how dominant their follower count is, with genre only counting as a bonus when it's actually there, because real DJ mixes almost never bother tagging a genre. The one rule I really cared about was that it should never silently guess wrong. It only auto picks when it's genuinely confident, and if it isn't it hands back a ranked list of candidates and an honest reason why (unresolved, low_confidence or no_mix) and leaves the final call to me. Out of the 164 artists, 105 came back with a confident best mix. The other 59 came back with honest reasons, and a few of the big names like Jeff Mills and Ricardo Villalobos genuinely just don't post long mixes on SoundCloud.
Sticking Points
A successful write that reported itself as a failure
Once it was deployed I tested the repost tool against my real account and it came back with Unexpected end of JSON input, which of course looks like it failed. Except it hadn't. The repost had actually worked and was sitting right there on my profile. It turns out SoundCloud answers some of its write requests with a completely empty 204 No Content body, and my HTTP layer was blindly calling response.json() on every 2xx response, which throws the second there's no body to parse. So every successful unlike, unfollow and unrepost was getting handed back to the user as an error. The fix was tiny, just check for the empty 204 case before trying to parse it, but I only found it because I actually hit the live endpoint rather than trusting a green test suite. All my tests were mocking full JSON bodies, so the bug was completely invisible to them.
The rate limit cascade that lied about who exists
Trying to resolve all 150 odd artists in big batches kept tripping SoundCloud's rate limiter, and the way it failed had me going for a while. Each artist resolution makes a few API calls, so when a big burst hit the limit those failed lookups were getting reported back as unresolved, which of course reads as "this artist doesn't exist". Ben UFO, one of the most famous DJs on the whole lineup, came back "unresolved" in a batch of 23 and then resolved instantly when I ran him in a batch of 3. It was a burst problem dressed up to look like a data problem. The fix came down to three things. Smaller batches, a cap on how deep the per artist track pagination is allowed to go, and an abort flag so that the moment any worker hits a 429 the rest of the batch stops instead of piling on and making it worse. When I later ran the whole branch through an adversarial code review, the reviewer landed on that exact same unbounded pagination as the root cause completely on its own, which was a nice bit of confirmation I'd fixed the right thing.
An OAuth race I designed away
The SoundCloud login uses a PKCE flow, and the first version of it stashed the PKCE verifier in Cloudflare KV keyed by the OAuth state parameter. The catch is that KV is eventually consistent, so when SoundCloud redirected straight back to me (which it does, because my account was already authorised so there's no consent screen to slow things down) the callback would sometimes try to read the verifier before the write had even landed. That gave me an intermittent "expired or unknown state" error that would magically fix itself on a retry. Rather than just papering over it I made the whole flow stateless. I encrypt the verifier and the original request into the state parameter itself using AES-GCM, then decrypt it again on the way back. No KV round trip, no race, and I baked a 600 second freshness check into the encrypted payload to keep the same expiry behaviour I had before.
How it was built
The whole thing was built test first and ended up with 124 tests covering the ranking logic, the resolver's confidence contract, the OAuth handlers and all the fiddly HTTP edge cases. Then I put it through a full adversarial code review before merging, which is exactly where the rate limit and empty body issues got pinned down and fixed properly under TDD. It was also a genuinely agentic build, which was half the fun. I worked through the whole thing with Claude Code, and once the server was live the agent didn't just write the code, it actually drove the tools against live SoundCloud to build the playlist. It pulled the lineup out of the festival flyer images, ran the dedup, called the resolver across all 164 names, helped me recover the tricky ambiguous headliners and then created the final playlist. Building the system and running it turned into the same loop, which was a really cool thing to watch.
Result
What I've ended up with is a live MCP server running on Cloudflare's edge and, better still, a real artifact to show for it. A 105 track Dekmantel 2026 playlist with one best mix per artist, all sequenced in festival day order. It's exactly the thing I wished was waiting in my inbox when that timetable first dropped, and it's genuinely how I'm getting to know this year's lineup now.
I'd also like to say that to do this it required upgrading to SoundCloud's Artist Pro account so I had access to their API. So for those other engineers thinking of building something the same, I'd advise doing this before you jump in! Happy coding!