substack-gateway-worker
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
substack-gateway-worker
A TypeScript port, for Cloudflare Workers, of
substack-gateway-oss by
Jakub Slys (MIT, Copyright (c) 2025 Jakub Slys), pinned at commit bc2e875.
Behaviour, messages and defaults follow upstream; port changes are listed under
"Differences from upstream" and in NOTICE.md. Port copyright (c) 2026 Jd Fiscus.
Unofficial. Not affiliated with Substack.
Deploy URL: https://deploy.workers.cloudflare.com/?url=https://github.com/iamfiscus/substack-gateway-worker
It exposes 51 REST endpoints under /api/v1 (OpenAPI at /api/openapi.json,
Swagger UI at /api/docs) and 49 MCP tools at /mcp. See
API reference, MCP,
Authentication, Draft Markdown,
Extensions and Deploy.
Credential modes
Pass-through (default). No secrets. Each caller sends their own Substack
credentials: x-gateway-token on REST, token argument on MCP, as
base64-encoded JSON {publication_url, substack_sid, connect_sid}. With
GATEWAY_API_KEYS unset the Worker is open, exactly like upstream.
Owner mode. Set GATEWAY_API_KEYS (comma list of label:key or
label:sha256:<hex>) plus OWNER_PUBLICATION_URL, OWNER_SUBSTACK_SID and
OWNER_CONNECT_SID as Worker secrets. Callers send Authorization: Bearer <key>
and use your cookies without ever seeing them. Everything except
/api/v1/health/live and / needs the key. An explicit token still wins and is
not feature-gated. Resolved credentials are never logged or echoed (show=true
returns 403).
Owner feature list (OWNER_ALLOWED_FEATURES, comma list, unset = default): by
default only reads (get/list and all stats), drafts:create,
drafts:update, drafts:prepublish, drafts:ai-detection, image upload and
health:ready work with your cookies, as api: and mcp: names. Creating
notes, comments, likes, deletes, scheduling and restacking are off until you list
them. Prefix entries with + to add them to the default, for example
+api:notes:create,+mcp:notes:create; a list without + replaces the default
entirely. Mixing the two forms is rejected as a misconfiguration. See
Authentication. The Worker never publishes or
schedules anything on its own, and has no crons.
Related MCP server: substack-publisher-mcp
Getting your cookies
Log into Substack in a browser, open developer tools, Application (or Storage),
Cookies, and copy the values of substack.sid and connect.sid.
Warning: these are full-account credentials. Anyone holding them can act as you on Substack: read, post, change settings. Never commit them, paste them into issues or chats, or send them to a Worker you do not control. Prefer a throwaway publication while you try this out.
Terms of Service risk
This uses Substack's internal, undocumented API with your session cookies. It is
not an official integration. Substack's Terms of Service (check
substack.com/tos yourself; this is not legal advice) restrict crawling and
scraping, copying or storing significant portions of content, processes that run
while you are not logged in, and sharing account credentials. Violations can
lead to account termination. Automation that runs on a schedule with your
cookies, bulk downloads and handing your cookies to other people are the
likeliest to cross those lines. This project ships no crons and no cookie-health
checker for that reason; keep volume low and human-triggered. Internal endpoints
can also change or vanish without notice. You use this at your own risk.
Trust model
In pass-through mode the operator's Worker sees callers' cookies: it decodes
every request's token in memory to call Substack. That is upstream's trust model
too. Only send your cookies to a Worker you run. In owner mode your cookies live
in Worker secrets (encrypted, not readable back through wrangler) and callers
never see them, but anyone with a gateway key can use the allowed features as
you. MCP tool arguments can land in agent transcripts, so avoid passing token
from an MCP client.
Limits
Measured in workerd by test/workers/runtime.test.ts, with the Cloudflare plan
limits as published at
https://developers.cloudflare.com/workers/platform/limits/ (checked
2026-11-07; re-check, they change):
Item | Measured or counted | Workers Free | Workers Paid |
CPU to convert a ~5,000-word draft ( | about 2 to 3 ms (the workerd timer is coarse, treat as approximate) | 10 ms CPU per request | 30 s default, up to 5 min |
Subrequests per draft write | createDraft: N | 50 per request | 10,000 default, up to 10M |
1 MB multipart image upload ( | 1 subrequest to Substack | counts against 50 | counts against 10,000 |
Memory per isolate | image bytes are base64-encoded in memory | 128 MB | 128 MB |
So creating a draft with 12 external images makes 12 + 1 + 1 = 14 subrequests (updating one makes 12 + 1 GET when expected_updated_at is sent + 1 PUT = 14, or 13 without it), which fits
the free plan's 50 but not far beyond it. The free plan's 10 ms CPU cap leaves
little room above the converter's few ms plus parsing and JSON; large drafts may
need Workers Paid. The maintainer's instance runs on Workers Paid.
Rotating cookies
Cookies expire or get rotated, and static secrets go stale. When requests start
returning 401, log into Substack, copy fresh values and run:
npx wrangler secret put OWNER_SUBSTACK_SID
npx wrangler secret put OWNER_CONNECT_SIDA 403 from Substack can be a bot-challenge page rather than an expired
session; try the same cookies in a browser first. Rotate GATEWAY_API_KEYS the
same way. Note: a raw gateway key that literally starts with sha256: is read as
a hash entry, so choose keys that do not.
Differences from upstream
Dropped (cannot work in a bundled Worker or ToS-driven):
lifespan hooks (Workers have no lifespan)
httpx connection pool settings
loading extensions from environment variables or entry points (extensions are listed statically in
src/index.ts)the optional cookie-health cron (it would be a process running while you are not logged in)
Changed behaviour:
Malformed Substack responses (HTML bot-challenge pages, empty bodies, non-JSON) surface as
502, not upstream's500.Note content is capped at 50,000 characters (
422on REST, a tool error on MCP), because pathological Markdown costs real CPU on Workers.Replying to a target that has no post returns a distinct
404.Redirects are never followed, so cookies are never sent off-host.
MCP tool-argument validation wording differs (zod, not pydantic).
Draft dialect, JS number limits:
1.0round-trips as1, integers above 2^53 lose precision, and attribute keys that look like integers are ordered first.Owner mode, the feature gate, hashed gateway keys and the optional KV caches are additions.
POST /api/v1/imagesand theupload_imageMCP tool refuse images over 10 MB (413 {"detail":"Image too large (max 10 MB)"}on REST, checked fromContent-Lengthbefore the body is read and again on the file size; an upload without a numericContent-Length, such as a chunked one, is411 {"detail":"Content-Length required"}; a tool error on MCP, whose transport already caps a request at 4 MB). Upstream has no cap.An incoming
X-Request-IDis reused only if it is at most 128 characters of[A-Za-z0-9._-]; anything else gets a fresh UUID.scheduled_atgiven as a numeric string ("1767225600") is a422; pydantic accepts it as a unix timestamp. Send a JSON number or an ISO-8601 string.A Substack user-settings entry without a
typeis accepted (onlyuser_idis read); upstream's model requirestypeand answers500.With a value cache installed (for example
kvValueCache), the public profile lookup behind the profile routes is fetched anonymously (empty cookies, as the MCP profile tools do), so a cached profile never depends on one caller's session and one caller's auth error is never served to another. Without a cache the caller's credentials are used, as upstream does.
Develop
npm install
scripts/fetch-upstream.sh # clones the pinned upstream reference into upstream/
npm test && npm run test:workers && npm run typecheck && npm run lintTool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
This server cannot be deployed
Maintenance
Related MCP Connectors
Hosted MCP for Instagram, Facebook, X, LinkedIn, Pinterest, and Ghost. OAuth 2.1, 38 tools.
Read and edit GA4, Search Console and Google Tag Manager from any MCP client. 29 tools.
Hosted MCP for Google Workspace, Atlassian and Notion: one OAuth, per-user accounts, write access.
Create, schedule, and publish social posts, manage accounts, and read analytics as MCP tools.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables programmatic management of Substack content, including creating drafts, publishing posts, and uploading images. It supports specialized features like live blogging and posting to Substack Notes through MCP-compatible AI tools.MIT
- AlicenseAqualityDmaintenanceA tool that connects to Substack's official Publisher API to access posts, newsletters, analytics, and subscribers, compatible with MCP clients like Claude and Cursor.63MIT
- AlicenseNot gradedqualityDmaintenanceEnables managing Substack publications: publish Notes and posts, manage drafts, pull analytics, and read any publication's public feed via MCP.MIT
- AlicenseNot gradedqualityBmaintenanceEnables managing Substack publications through 27 tools for drafting, publishing, subscriber analytics, comments, Notes, and reader feed via Streamable HTTP.219 npm1MIT