Strategy ↗

How we publish to our website with no admin login

FM publishes through a Cloudflare Workers MCP server, gated by Microsoft Entra. No admin login, no user table, no CMS, no /forgot-password page.

I published this article without logging in to a website. There is no admin page on fmcybersecurity.com, no /login, no /forgot-password flow, no user table. I opened Claude Desktop on my laptop, walked through the draft with Claude, and asked it to publish. The MDX hit our GitHub repository, Cloudflare rebuilt the site, and the page was live in roughly 90 seconds.

This is the publishing flow for every consultant at FM CyberSecurity who is authorized to publish. Same Claude Desktop, same laptop, same MCP server we wrote.

In four client web platform reviews I ran last year, the publishing flow ate weeks of project time. Adding a new content editor was a ticket. Removing one was another ticket, plus a password reset, plus a license-seat decision. The CMS was the bottleneck on a process that should be a Git commit. We built around it.

Last reviewed 2026-05-12 by Fredrik Standahl.

TL;DR: FM has no admin interface on its website. Authors publish through Claude Desktop calling fm-publisher-mcp, a Cloudflare Worker we wrote, gated by Microsoft Entra group membership. The MCP commits MDX and a cover image to GitHub atomically. The new page is live in roughly 90 seconds. Three wins: fewer vulnerabilities (no admin, no user table, no passwords), faster publishing (write in Claude, live in 90 seconds), and better content per draft because Claude runs a writing skill we wrote with FM’s voice rules baked in.

How a consultant publishes today

Here is what happens when I publish. I open Claude Desktop on my laptop and click Connect on fm-publisher-mcp. A local OAuth handler opens a browser tab to Microsoft. If I already have an active M365 session, the prompt skips. Microsoft returns an auth-code to our Cloudflare Worker, which exchanges it for an access-token, validates the JWT against Entra’s JWKS endpoint, and calls Microsoft Graph to check whether I am still in the Entra group called “FM Publishers.”

If the answer is yes, the Worker issues its own access-token (1 hour TTL) and refresh-token (30 day TTL) to Claude. I write or revise the article in Claude Desktop, ask Claude to publish, and Claude calls write_article on the MCP with the MDX body and the cover image in one call. The Worker commits both files to GitHub through the Git Data API as a single atomic commit. MDX and cover image land together, or neither does. Cloudflare’s build picks up the commit and rebuilds the site. The new page is live in roughly 90 seconds.

I see four moments in that flow. Opening Claude Desktop, clicking Connect, writing the draft, asking Claude to publish. The other five run without my attention.

The stack underneath

fm-publisher-mcp runs on a Cloudflare Worker. It exposes eight tools (list_articles, read_article, create_article, update_article, unpublish_article, plus three for the attack-list). Cloudflare’s @cloudflare/workers-oauth-provider package handles the OAuth 2.1 server side. We wrote the broker against Microsoft Entra ourselves, in roughly 450 lines of TypeScript, so the Worker can swap a user’s Entra identity for its own bearer token without holding a password.

There is one Workers KV namespace called OAUTH_KV. It holds tokens the Worker issued. It does not hold user data. There is no users table, no email-to-password map, no profile store. The total persisted state about a publisher is the token expiry row, and that row evaporates within 30 days of last use.

The website itself runs on Astro 6 with output: static, deployed to Cloudflare Pages. Astro 6 shipped in March 2026 and the Astro team now sits inside Cloudflare after the January 2026 acquisition, which means the framework and the host share a roadmap. Cloudflare is folding Pages into Workers, and we will migrate when the cost is below the cost of staying.

Deploys are atomic. Cloudflare’s own documentation calls them that. When the build succeeds, the new version cuts over in one operation. When it fails, the previous version keeps serving traffic. We have not lost a request to a deploy. Rollback to a previous build is one click from the Pages dashboard, or one API call when we need it from the terminal. Time-to-first-byte from Norwegian cities measures 30 to 50 ms.

What this buys us

No admin login on fmcybersecurity.com. There is no surface to phish, no /login to brute-force, no session cookie to steal, no SSO button on a public-facing page to point a phishing kit at. The website authenticates nobody, because the website does not need to.

No user table. We do not store who can publish, Microsoft Entra does. Adding a publisher is one click in Entra (add to “FM Publishers”). Removing one is one click in Entra. Within the Worker’s access-token TTL, the change takes effect. If the person leaves FM entirely and their M365 account is deactivated, every existing token they hold dies the next time it is presented, because the JWT validation fails against the rotated keys.

Audit trail is Git. Every commit to the site repo names the human author (pulled from the Entra token) and the bot committer (fm-publisher[bot]). The audit is git log. We do not run a parallel publishing-event database, and we do not need one.

Zero passwords stored anywhere in this stack. We do not run a /forgot-password flow because there is nothing to recover. Authentication is delegated end-to-end to Microsoft Entra, which already handles MFA, conditional access, device compliance, and the rest of the M365 controls we already pay for.

Better content per draft. We wrote a Claude skill (fm-website-writer) that encodes FM’s voice rules, banned words, six article-type templates, SEO and GEO mechanics, and our marketing-claims discipline under Norwegian markedsføringsloven. Claude runs that skill on every article it drafts inside this MCP. The first time we read a draft, the structure is already right, the banned words are gone, the SEO frontmatter is filled. The human review is for the argument, not for fixing “leverage” or em-dashes for the hundredth time.

There is also a procurement angle. If a website supplier runs a user database, that is a DPA you sign and an audit you carry. If they do not, it is not. Smaller surface, smaller paperwork, smaller liability.

Be honest about what site you have

This trade exists because we picked a problem with a static answer. A consulting firm’s website personalizes nothing, accepts no visitor logins, runs no checkout. Content flows in slowly enough that human review is the bottleneck, not the deploy.

A site that personalizes per visitor, accepts user accounts, or processes payments is a different problem. It will be a running application. That is fine, the answer is to build it as one, not to bolt static on top.

The point is not that static sites are simpler. The point is to separate what has to run on a server from what is a file, and to put authentication one layer up in the identity provider you already run. Most B2B marketing sites are files. The publishing flow is the only part that needs to authenticate anyone, and authentication does not have to live on the website itself.

If you own a B2B marketing site on a CMS today, the question is not whether to migrate. The question is whether the running cost (license, patches, admin login as identity surface, vendor risk on every CMS update) is paying for features you use. List the features. Most of the time, the list is shorter than the bill.

Drafted with AI assistance, reviewed and edited by Fredrik Standahl and the FM editorial team.

← Back to all insights
Questions or inquiry? [email protected] Contact us →