Shared countdown timers that stay in sync on every screen β no account, any number of viewers.
Copy the AI prompt to install this server into Claude Code, Cursor, or another agent β or use 1-click editor setup below.
π‘ Paste the JSON block into your client's configuration file under mcpServers, then restart the application.
A free, zero-backend countdown timer. Set a duration, copy the link, send it to a room β everyone who opens that link sees the identical countdown, perfectly in sync, because the deadline is a timestamp encoded in the URL itself. No account, no server, no database.
Live at countlink.app. For local dev, open index.html
directly or serve the folder β see below.
License: source-available, not open source. No LICENSE file means all rights reserved by default β this repo is public so the "nothing you paste is uploaded" claim is checkable, not an invitation to redistribute or relaunch it as your own product.
Full research and reasoning is in the project memory (see /Users/bruno/.claude/projects/-Users-bruno-Code-boring-apps/memory/),
but the short version: single-purpose "boring" utility sites (vClock, Wheel of
Names, word counters) generate real, verifiable ad revenue at near-zero
operating cost. vClock specifically β a free online timer β earns an
estimated ~$500K/yr from Google AdSense on ~5.4M monthly visits. CountLink
copies that proven business model (free tool, one ad slot, programmatic SEO
pages) while adding a genuine feature vClock doesn't have: shareable,
synced-by-link countdowns.
No build step, no dependencies. Any static file server works, but use the
included one during development β it sends Cache-Control: no-store so
edits always show up on reload (a plain file server can leave your browser
serving stale CSS/JS after a change):
(This is also what .claude/launch.json runs when using the Claude Code preview.)
assets/app.js writes the countdown's end-timestamp and label into the URL
hash on start, e.g. #t=1783378107213&l=Workshop%20resumes. Opening that same
URL on any other device reads the same timestamp and counts down to it using
the device's own clock β so there's nothing to host, nothing to keep running,
and no possibility of the "server" going down.
#t= vs #for=There are two shapes of link, and the difference is the product:
| Shape | Means | Written by |
|---|---|---|
#t=<epoch ms> | a running countdown, one fixed instant every screen agrees on | makeLink(), when someone presses start |
#for=<duration> | a setup link β the board opens preloaded at that duration, ready but not started | anyone, in advance, by hand |
#for= exists because until it did, only a human in a browser who had
already pressed start could produce a working CountLink URL β the share link
carries an absolute timestamp, so nothing that doesn't know the current epoch
time could write one. That ruled out every context that wants to write a timer
down in advance: an AI assistant answering "give me a 25 minute shared timer"
(the largest traffic channel this site has), a bookmark, a calendar invite, a
lesson plan, a saved OBS scene.
It accepts the same grammar the board accepts on paste β 25m, 1h30m,
90s, 5:00, 1:30:00, 45 β by calling the very same
parsePastedDuration(), so there is only one duration syntax on the whole
site. Add a label with &l=. Works on any timer page.
A setup link deliberately does not start itself. If it did, three people
opening the same #for=25m would each get their own countdown from whenever
they clicked β the per-visitor "evergreen countdown" deliberately rejected in
docs/ and the exact opposite of "one fixed instant, identical on every
screen". e2e/setup-link.spec.mjs fails if that ever changes.
The one exception is &go=1, which starts on load. It is opt-in precisely
so "a shared link never auto-starts" stays a visible rule rather than a hidden
per-page exception, and it exists for the OBS overlay β a Browser Source is one
scene on one machine with every control stripped out and no link to copy from
it, so there is no second viewer to fall out of sync with. The full overlay URL
is /embed/?overlay=1#for=10m&go=1; both halves matter β /embed/ is the
ad-and-analytics-free build, ?overlay=1 is what actually makes it
transparent. Never hand out /?overlay=1 (see docs/overlay-ads.md: it
redirects to /embed/ too late to stop the preload scanner queueing the ad
scripts, and ads on a content-free screen is a live AdSense violation).
/mcp)functions/mcp.js is a Cloudflare Pages Function exposing CountLink over the
Model Context Protocol, so an assistant can hand someone a working link
rather than telling them to go and make one. Four tools, all genuinely
read-only (there is no backend β a timer is string arithmetic over a
duration): create_timer, create_agenda, create_badge and
describe_timer_link.
create_agenda takes an ordered list of { duration, label? } segments and
returns one link on /timers/agenda-timer that starts now (or at an optional
start_at) and advances through every segment on every screen, plus a run
sheet. It mirrors encodeAgendaHash() in assets/app.js byte for byte and
is round-tripped through that page's own parseAgendaHash() in the tests.
describe_timer_link reads agenda links too, including a scheduled one that
hasn't started yet.
A scheduled start_at in the future shows a "Starts soon" countdown, not an
inflated first segment β computeAgendaState()'s idx gained a -2
("not started") sentinel alongside the existing -1 ("finished"), because
every segment boundary being positive meant any negative elapsed time used
to resolve to segment 0, folding the wait into that segment's remaining time.
Mirrored in functions/mcp.js's describeUrl(). e2e/agenda.spec.mjs
covers the live transition β a 2-second-out agenda actually ticking over
into its first segment with no reload, across all 5 browser projects.
create_timer covers three shapes with boolean flags: a plain shared link by
default, for_obs_overlay: true for a self-starting OBS Browser Source, and
embed_on_website: true for a ready-to-paste <iframe> snippet for someone's
own web page. The last two are NOT the same URL shape despite both routing
through /embed/ β OBS is one viewer, so its link auto-starts per load
(#for=β¦&go=1); a website has many visitors, so its link must be a single
fixed instant (#t=β¦) they all see identically, or it degenerates into the
"evergreen per-visitor countdown" this product exists not to be. The website
embed also carries the attribution <a> OUTSIDE the <iframe> β a link
inside a frame is attributed to the frame's own document, not the host page,
so it earns no backlink; this is the one mechanism on the whole site that
compounds links without per-instance outreach (see docs/monetization.md).
No reviews yet β be the first to share how this listing worked for you.
Showcase your server listing on GitHub or your project documentation. Embed this dynamic SVG badge to highlight official listing status and live engagement.
[](https://allmcps.com/mcp/countlink)<a href="https://allmcps.com/mcp/countlink"><img src="https://allmcps.com/api/badge/countlink?style=directory" alt="CountLink on AllMCPs" /></a>