Building a Blog That ChatGPT Can Edit with MCP and Passkeys

Published 31 May 2026

How I added an MCP server, OAuth, and passkey-based authorization to make my blog editable by ChatGPT without sharing passwords or static API tokens.

I wanted this blog to stay simple.

Write posts. Edit them quickly. Publish without ceremony. Keep the public site fast.

But I also wanted to try something more interesting: I wanted ChatGPT to help manage the blog directly.

Not by copying text into an admin panel. Not by sharing a password. Not by handing over a long-lived API token and hoping for the best.

I wanted a proper tool interface.

So I added a custom MCP server to the blog, then protected it with OAuth and passkeys.

What MCP Adds

MCP, or Model Context Protocol, gives AI clients a structured way to talk to external tools.

Instead of asking ChatGPT to write some markdown and then manually pasting that markdown into a CMS, the blog exposes real actions that an AI client can call:

  • create_post
  • update_post
  • delete_post
  • get_post
  • list_posts
  • search_posts
  • latest_posts

That changes the workflow quite a bit.

ChatGPT can create a draft, search existing posts, fetch a post before editing it, update the markdown, and return the post URL when it is done.

It feels less like using a text generator and more like working with a collaborator that has access to a small, well-defined set of tools.

Why I Moved Beyond a Static API Token

The first version used a simple API token. It worked, but it did not feel like the right long-term shape.

A static token has a few problems:

  • It is easy to over-share.
  • It is hard to rotate cleanly.
  • It does not map well to real user authorization.
  • It gives an agent access without a proper login and consent flow.

For a personal prototype, that might be fine. But if AI agents are going to edit real content, authentication should be more deliberate.

So I replaced the API token with OAuth and passkey-based login.

Using Passkeys for Authorization

The login flow now uses WebAuthn.

When an MCP client wants access, it starts an OAuth authorization flow. The browser opens, I authenticate with my passkey, approve the request, and the MCP client receives an access token.

The important part is that the AI agent never sees my passkey. It only receives scoped OAuth credentials after I approve the flow.

The access token is short-lived, and the refresh token is rotated. That keeps the workflow usable without turning the credential into something permanent.

The Shape of the System

At a high level, the system looks like this:

ChatGPT
  -> /mcp
  -> OAuth challenge
  -> passkey login
  -> access token
  -> MCP tools
  -> D1 posts database

The public blog reads from Cloudflare D1. The MCP tools write to the same database. Astro renders the public pages, and Cloudflare Workers handle the server-side routes.

The OAuth and MCP routes live alongside the blog:

/.well-known/oauth-protected-resource
/.well-known/oauth-authorization-server
/oauth/register
/oauth/authorize
/oauth/token
/login/passkey
/admin/passkeys
/mcp

That keeps the whole setup self-contained. The public site is still just a blog, but there is now a secure tool layer underneath it.

Designing the MCP Tools

One lesson from building this was that MCP tool responses should be intentionally small.

At first, write tools returned the full post object. That included the title, description, markdown body, tags, timestamps, status, and more. It worked, but it was noisy and wasted tokens.

Now write operations return a compact response with the important details:

{
  "ok": true,
  "action": "updated",
  "slug": "example-post",
  "status": "published",
  "url": "https://example.com/posts/example-post"
}

If the agent needs the full markdown body, it can explicitly call get_post.

That makes the editing loop cleaner. The default response confirms what changed and gives the user a link, without flooding the conversation with content the model may not need.

Caching Gotchas

The public pages were originally cached in the Worker cache.

That caused a subtle issue: ChatGPT could update a post successfully, but the browser would still show the old version.

The fix was simple. The homepage and post pages now use:

Cache-Control: no-store

For a small personal blog backed by D1, always-fresh reads are worth it. The site stays simple, and edits appear immediately.

A better future version would reintroduce caching with explicit invalidation when posts are created or updated, but no-store is the right tradeoff while the editing workflow is still small and simple.

Why This Feels Useful

The end result is still a minimal blog. There is no heavy CMS, no complicated editorial dashboard, and no new publishing ceremony.

But now I can ask ChatGPT to:

  • draft a new post
  • rewrite an intro
  • fix typos
  • update code snippets
  • search for related posts
  • publish a finished draft

Because those actions go through MCP, they are structured. The tool names are explicit, the inputs are typed, and the outputs are predictable.

That is the kind of AI integration I like: not a chatbot bolted onto a website, but a small set of real tools exposed safely.

What I Would Improve Next

There are a few obvious next steps:

  • add preview URLs for drafts
  • add revision history
  • add a dedicated publish_post tool
  • add stronger validation before publishing
  • add image upload support
  • improve the admin page for passkeys and token management

Even in this version, though, the workflow already feels good.

The blog is still just a blog. The difference is that now it has an AI-editable backend, protected by passkeys, exposed through MCP, and running on Cloudflare.