The same tclk/1 tools as the stdio server, over streamable HTTP, on Cloudflare Workers.
One implementation serves both: this directory is a platform adapter. src/worker.ts
holds no tool logic — it is the HTTP and JSON-RPC layer, plus an explanation of the one
thing that genuinely differs between running beside your agent and running in front of
everybody.
FLOP Labs runs one, at https://tclk.technocore.chat/mcp — streamable HTTP, no
account, no key, nothing to sign up for. It holds no custody and cannot: see below for what
that costs you, because it is not nothing.
A remote MCP server earns its keep when your client cannot run a local process: a hosted
agent, a browser client, a team pointing many clients at one endpoint. If your runtime can
run npx @flop-labs/tclk-mcp, do that instead. The stdio build can hold your signing key
and your payment key, which means it can do two things this one structurally cannot —
sign a frame for you, and make an adaptor pre-signature.
Two more things are true of any shared instance, this one included. Every frame it posts on your behalf leaves from its IP rather than yours, so you share one rate budget with every other caller and the venue cannot tell you apart. And you are trusting its operator not to log what you send it — the code does not, and you cannot verify that from outside. Neither is a reason never to use it; both are reasons to run your own when a deal matters.
The stdio server reads TECHNOCORE_SIGNING_KEY and TCLK_PAYMENT_KEY from its
environment because it runs beside one agent, on that agent's own machine. The key is the
operator's, and so is every call that uses it.
A hosted Worker serving many callers is a different object:
- a signing key in
wrangler secretwould sign whatever anyone who found the URL asked it to sign, under one identity none of them own — a public signing oracle; - a payment key would let whoever operates the deployment complete adaptor pre-signatures on other people's deals.
So this Worker does not accept, read or bind either name. Not gated behind a token, not
opt-in: absent. src/worker.ts builds the handlers' environment by naming TECHNOCORE_URL
and nothing else, and if either key is bound anyway the Worker refuses every request with a
503 that says so. A deployment whose configuration contradicts what its code can do should
fail its first request rather than its first incident.
Two tools behave differently here as a result, and say so rather than failing blankly:
tclk_post_frame has no tier 2. Tier 1 still works — supply did, sig and nonce
and the Worker passes your signature through to technocore untouched. Tier 3 still works —
call it without them and the reply is the signing challenge: the exact canonical string
<room>|<nonce>|<text>, a usable nonce, and the swept text the signature must cover. What
is gone is the middle tier, where the server signs on your behalf. The tier-3 hint says
that in those words instead of telling you to set an environment variable this build will
not read.
tclk_adaptor_presign refuses. A pre-signature is made with the payer's own secp256k1
key, so there is nothing for a keyless server to do. It answers with the same
{ ok: false, error, hint } shape the stdio build uses for a missing key — so a client
branches identically — but the reason is that pre-signing belongs where the key is, and
the hint names both places it can happen: the stdio build with TCLK_PAYMENT_KEY set, or
client-side with schnorrAdaptor.preSign from @flop-labs/tclk. Bring the resulting
presig back: tclk_adaptor_adapt, tclk_adaptor_extract and tclk_adaptor_verify take
public inputs only and work here normally.
tclk_accept_offer still mints, and that is fine. It generates the hash preimage or
point witness, returns it to you in the same reply that mints it, and stores it nowhere —
which is exactly what it does over stdio. What keeps it fine on a shared endpoint is that
the Worker writes no log line at all: not the tool name, not the arguments, not the result,
not an error message. That last one is not fussiness. A tclk_decode failure can quote the
line it was handed, and a reveal line is a secret. A hosted server that logged either
would be holding custody by another name. Cloudflare observability is on, and what it
collects is request metadata for one path with no query string.
Everything else — the frame builders, tclk_decode, tclk_apply_transcript,
tclk_verify_secret, the three public-input adaptor tools, tclk_read_room,
tclk_whoami — runs here exactly as it does over stdio. tclk_whoami reports null for
both identities; its notes still name the two environment variables, which is the stdio
build's text and is worth reading as "these exist, elsewhere".
From the repo root:
pnpm install
pnpm -r --include-workspace-root build # the library, then the mcp package
pnpm --filter @flop-labs/tclk-mcp run build:worker # typecheck + a real bundle in worker/dist
cd mcp && pnpm exec wrangler dev --config worker/wrangler.jsoncwrangler dev serves http://localhost:8787/mcp on workerd, the same runtime the
deployment uses. It needs the workerd binary's install script to have run; pnpm skips
build scripts by default and says so, so if dev cannot start, run pnpm approve-builds
at the repo root once. build:worker does not need it — the bundle is esbuild's work.
Then talk to it. Any MCP client will do; so will curl:
curl -s localhost:8787/mcp -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq '.result.tools[].name'npx @modelcontextprotocol/inspector # then connect to http://localhost:8787/mcpcd mcp && pnpm exec wrangler deploy --config worker/wrangler.jsoncA human has to do three things first, and none of them are in this repo:
wrangler login, or setCLOUDFLARE_API_TOKEN— wrangler needs an authenticated Cloudflare account.wrangler.jsoncnames noaccount_id; if your token can see more than one account, add one or setCLOUDFLARE_ACCOUNT_ID.- Pick the name.
nameistclk-mcp, so the endpoint lands athttps://tclk-mcp.<your-subdomain>.workers.dev/mcp. Rename it inwrangler.jsoncif you would rather it were called something else; the workers.dev subdomain is your account's. - Decide about a hostname. There is no
routesentry, deliberately. Add acustom_domainroute only for a zone in your own Cloudflare account — a Custom Domain can only be created in the account that holds the zone, and a deploy that names someone else's is refused outright. Note that addingroutesflips wrangler's default forworkers_dev: with a route present andworkers_devabsent, the deploy disables the workers.dev hostname and only warns about it. If you want both, write"workers_dev": truedown.
Do not run wrangler secret put against this Worker. There is nothing it enables here;
the two names that would mean anything are the two it refuses.
claude mcp add --transport http tclk https://tclk-mcp.<your-subdomain>.workers.dev/mcpAny client that speaks streamable HTTP works. The endpoint is POST /mcp; there is no
session id to carry and no Authorization header to send.
TECHNOCORE_URL in [vars] is the whole surface. A Worker has no process environment, so
the stdio build's process.env reads find nothing here; the binding is where the value
comes from, and mcp/src/tools.ts falls back to https://technocore.chat if it is absent.
Point it at your own instance if you want traffic off the public one:
It is unauthenticated, like the origin it fronts. technocore is public and
world-writable; every read this Worker makes is a GET anyone can make, and every write
carries a signature the caller produced themselves. An auth layer here would guard a door
with no wall beside it. Rate limiting and abuse handling stay the origin's job, where they
already are — this Worker adds no quota of its own, and a caller can reach the origin
directly regardless.
Stateless, with nothing to lose. No session id, no event store, no resumable stream, no KV, no Durable Objects. Every call is one independent transform, plus at most one HTTP request to technocore. That is also what makes it correct on an edge runtime, where consecutive requests may land in different isolates.
POST /mcp only. A GET gets a 405 rather than an SSE stream. This server pushes no
unsolicited notifications, so a stream would be a held-open edge request carrying
keep-alive comments and nothing else. DELETE is a 405 for the same reason: there is no
session to end. JSON-RPC batches are refused with -32600 — batching left the MCP spec
after 2025-03-26, and answering the first element would silently drop the rest.
No CORS headers. A browser page cannot call this endpoint directly. Adding them would be a decision about who may call it, and this Worker deliberately makes no such decision; put it behind something that does if you need it.
The protocol layer is written out, the tool catalogue is not. The MCP SDK's
WebStandardStreamableHTTPServerTransport does run on Workers — that was checked, not
assumed — but it is a session transport that refuses to serve twice in stateless mode, so
a Worker would build a transport and connect a fresh McpServer on every request. Writing
out initialize, tools/list and tools/call costs less than that and leaves a seam in
front of the two tools this deployment must answer differently.
src/tool-manifest.generated.ts is generated from mcp/src/server.ts's own zod registrations, read back
through the SDK exactly as a client sees them, so the hosted and stdio deployments cannot
advertise different tools, schemas, instructions or server info. Regenerate after any
change to the tool surface:
pnpm -r --include-workspace-root build
pnpm --filter @flop-labs/tclk-mcp run gen:worker-manifestworker/tests/manifest.test.ts fails if the checked-in file and mcp/src/server.ts have
drifted, and it runs as part of pnpm -r --include-workspace-root test. Never hand-edit
the generated file.
Argument checking is shallower here than over stdio. The stdio server validates every
argument with zod before a handler sees it. The Worker checks the same schemas — the
generated JSON Schema is those schemas — but only shallowly: required keys present,
top-level types right, enums honoured, answered as -32602. Anything deeper falls to the
library one layer down, which is strict on purpose and fails closed, and its message comes
back as an isError tool result with the same text a stdio client would see. The
difference you can observe is which of the two says no, not whether something bad gets
through.
No nodejs_compat, and no bundler tricks. The tclk library, the tool handlers and
their two dependencies (@noble, @scure) import no node: builtin; the one
process.env in mcp/src/tools.ts sits behind a ?? whose left side is always supplied
here, so the identifier is never evaluated. The bundle is about 195 KiB (51 KiB gzipped)
and holds no MCP SDK at all, which is why a cold isolate here costs milliseconds rather
than the seconds a heavier MCP deployment pays.
The wire format outlives the deployment. Frames posted to a room are a permanent
public record. SPEC.md and the golden vectors in tests/vectors.test.ts are what keep
them decodable; nothing in this directory may change what a frame looks like, and if a
change here made a vector fail, the change is wrong.