DEV Community

Rob
Rob

Posted on • Originally published at vibescoder.dev

Adding an MCP Server to the Blog Itself

Two weeks ago I wired MCP into my fitness tracker — ten tools, one endpoint, four clients. That was always a test run. The fitness tracker is a low-stakes app. If an agent writes a bad workout entry, I delete it. The blog is different. The blog has published content, a deploy pipeline, an editorial calendar, analytics, syndication to Dev.to. If an agent publishes a draft that wasn't ready, the internet sees it.

This week I added an MCP server to vibescoder.dev anyway. Sixteen tools across five categories. The agent that helped me build it — running in a Coder workspace — can now turn around and use it to manage the very site it just modified. That's the kind of loop that makes building in public feel recursive.

The Goal

One sentence: let any agent directly publish to the site, analyze traffic data, and troubleshoot production issues.

The blog is a Next.js 16 app deployed on Vercel. Content lives in a separate private GitHub repo (the-vibe-coder-content), committed via the GitHub API. The admin UI already supports voice recording → Claude-generated MDX → one-click publish. But the admin UI requires a browser. An agent in a Coder workspace, or in Claude Desktop, or in Cursor can't click buttons. MCP gives them the same capabilities programmatically.

Architecture

The fitness tracker MCP server talked to Postgres via Prisma. This blog has no database. Content is MDX files in a GitHub repo. Analytics are Redis counters in Upstash. Deployments happen by curling a Vercel webhook. So the MCP server is a GitHub API client, a Redis reader, and an HTTP caller — not a database wrapper.

Agent (Claude / Cursor / Coder Agents)
  │
  │  Streamable HTTP (Bearer token)
  ▼
vibescoder.dev/api/mcp/mcp
  │
  ├─ Content tools ──→ GitHub API (read/write/commit MDX)
  ├─ Analytics ──────→ Upstash Redis (view counters)
  ├─ Deploy ─────────→ Vercel deploy hook
  ├─ Syndication ────→ Dev.to API
  └─ Diagnostics ────→ fetch() against live site
Enter fullscreen mode Exit fullscreen mode

Same stack as the fitness tracker: mcp-handler for the Next.js adapter, zod for parameter schemas, bearer token auth, disableSse: true for stateless Vercel deployment.

The 16 Tools

The fitness tracker had 10 tools that all talked to one database. This server has 16 tools that talk to four different backends. Grouped by what they touch:

Content Management (7 tools) — the core editorial workflow:

server.tool('list_posts',     /* filter by status/tag/date */)
server.tool('get_post',       /* full MDX + frontmatter    */)
server.tool('create_post',    /* commit new MDX to GitHub  */)
server.tool('update_post',    /* partial frontmatter/body  */)
server.tool('publish_post',   /* draft → live, trigger deploy */)
server.tool('unpublish_post', /* live → draft, trigger deploy */)
server.tool('delete_post',    /* remove from GitHub        */)
Enter fullscreen mode Exit fullscreen mode

Blog Fodder & Editorial (4 tools) — the content pipeline:

server.tool('list_fodder',  /* active + archived, with consumption status */)
server.tool('get_fodder',   /* read raw session notes */)
server.tool('get_todo',     /* editorial calendar     */)
server.tool('update_todo',  /* maintain the calendar  */)
Enter fullscreen mode Exit fullscreen mode

Analytics (1 tool), Deploy & Syndication (2 tools), Diagnostics (2 tools):

server.tool('analytics_summary', /* 30-day views + top pages */)
server.tool('trigger_deploy',    /* hit the Vercel webhook   */)
server.tool('syndicate_post',    /* cross-post to Dev.to     */)
server.tool('site_health',       /* fetch key endpoints      */)
server.tool('get_settings',      /* AI style prompt config   */)
Enter fullscreen mode Exit fullscreen mode

Every tool returns raw data. The agent does its own analysis — same philosophy as the fitness tracker. The list_posts tool returns frontmatter for every post; the agent decides what "recent drafts" means.

What I Reused

The blog engine already had all the backend logic. The admin UI's API routes do the exact same operations — read a post from GitHub, commit an update, hit the deploy hook, cross-post to Dev.to. The MCP server calls the same library functions, not the HTTP routes:

import { commitFile, readFile, deleteFile } from "@/lib/github";
import { listDirectory } from "@/lib/github-list";
Enter fullscreen mode Exit fullscreen mode

The only net-new code was the directory listing helper (github-list.ts). The existing github.ts had file-level CRUD but couldn't list a directory. One function, 30 lines, wraps the GitHub Contents API for directory paths.

The auth pattern, CORS, and rate limiting were copied from the fitness tracker and adapted. Same timingSafeEqual, same withMcpAuth wrapper, same in-memory rate-limit buckets. The muscle memory from the fitness tracker build meant the security layer took minutes, not an hour.

The Middleware Change

One line. The blog's middleware protects all /api/* routes with JWT cookie auth. The MCP server does its own bearer-token auth. So /api/mcp/ gets added to the allow-list alongside /api/auth/, /api/analytics/track, and /api/slack/:

pathname.startsWith("/api/mcp/")
Enter fullscreen mode Exit fullscreen mode

The MCP route then handles auth independently — same pattern as the fitness tracker, where the middleware allow-listed the MCP path and the route enforced its own bearer token.

Decisions

Three questions came up during planning:

Auth granularity — single token or read-only vs. read-write tokens? Single token. I'm the only user. If I ever add collaborators, I'll add scoped tokens. Until then, one token does everything.

Audit logging — the fitness tracker writes to a Postgres audit_log table. This blog has no database. Options were Redis, console.log, or skip. I went with console.log (captured by Vercel function logs) plus [mcp] prefixed commit messages for every GitHub write. That gives me two audit trails — Vercel logs for all operations, Git history for content changes — with zero infrastructure.

[mcp] post: create "adding-mcp-server-to-the-blog"
[mcp] post: publish "adding-mcp-server-to-the-blog"
[mcp] chore: update TODO.md
Enter fullscreen mode Exit fullscreen mode

Image uploads — deferred. MCP tool parameters are JSON. Binary images would need base64 encoding in a tool call. That's doable but not worth the complexity in v1. The admin UI handles images fine. If an agent needs to add images to a post, it can use the admin API directly or I'll add an upload_image tool later.

The Template Update

Same Coder template pattern as the fitness tracker. Token flows from the workstation to workspaces:

/etc/coder.d/coder.env
  → TF_VAR_vibescoder_mcp_token
    → coder_agent.main.env (VIBESCODER_MCP_TOKEN)
      → jq merge into ~/.mcp.json at workspace start
Enter fullscreen mode Exit fullscreen mode

Three terminal commands on the homelab to finish it:

echo 'TF_VAR_vibescoder_mcp_token=<token>' | sudo tee -a /etc/coder.d/coder.env
sudo systemctl restart coder
cd ~/coder-templates && git pull && ./docker/apply.sh
Enter fullscreen mode Exit fullscreen mode

The gh auth login step was an amusing detour — I was SSH'd into the homelab from my iPhone, and gh tried to open a browser on a headless server. The fix was manually entering the one-time code at github.com/login/device in Safari. Mobile homelab administration is an underappreciated genre of suffering.

Verifying in Production

The real test was hitting the live endpoint:

curl -s -X POST https://vibescoder.dev/api/mcp/mcp \
  -H "Authorization: Bearer $VIBESCODER_MCP_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
       "params":{"protocolVersion":"2025-03-26",
                 "capabilities":{},
                 "clientInfo":{"name":"test","version":"1.0.0"}}}'
Enter fullscreen mode Exit fullscreen mode

Response: 200 OK, server name vibescoder, version 1.0.0, tools capability enabled.

Then a real tool call — list all drafts:

{
  "count": 1,
  "posts": [{
    "slug": "syndicating-to-substack-the-undocumented-path",
    "title": "Syndicating to Substack: The Undocumented Path",
    "published": false,
    "publishAt": null
  }]
}
Enter fullscreen mode Exit fullscreen mode

One draft in the queue. Real data from the content repo, returned through the MCP server, verified from a Coder workspace. The analytics tool came back with 660 views over 30 days and today's top pages. The site health tool checked five endpoints and reported status codes and response times.

The Recursive Moment

The part that's hard to describe until you experience it: the agent that helped build this MCP server can now use it. In the same chat session where we wrote the route file and debugged the middleware, the agent can call list_posts to see what's published, get_todo to check the editorial calendar, and trigger_deploy to ship changes.

This post was written in a Coder workspace. The MCP server it describes is live on the same site it will be published to. The agent could, in theory, publish this very post by calling publish_post with the slug. It won't — I'll review it first — but the capability is there. That's the loop.

What's Next

  1. Watch how agents use the tools in practice. The fitness tracker MCP server taught me that agents are surprisingly good at synthesizing raw data into summaries. Curious whether editorial tools — create, publish, schedule — feel as natural.
  2. Add an upload_image tool. Deferred from v1, but it's the obvious gap. An agent that can create a post but not attach images is writing with one hand.
  3. Update the vibescoder-blog skill file. The skill currently documents the Git-based editorial workflow. Now that the MCP server exists, the skill should point agents to the tools instead of the grep and awk one-liners.
  4. Write it up as blog fodder. Done. You're reading it.

By the Numbers

  • 16 MCP tools across 5 categories
  • 4 backends wired through one endpoint (GitHub API, Upstash Redis, Vercel deploy hook, Dev.to API)
  • 7 files changed in the engine repo, 2,365 lines inserted
  • 1 file changed in the Coder template repo, 23 lines inserted
  • 3 npm packages added (mcp-handler, @modelcontextprotocol/sdk, zod)
  • 1 middleware line to allow-list /api/mcp/
  • 0 new infrastructure — no database, no Redis, no queues. GitHub API + console.log
  • 3 terminal commands to update the homelab Coder config
  • 1 iPhone-to-homelab SSH detour for gh auth login via Safari
  • 660 views over 30 days — the first number the analytics tool reported back
  • 1 draft in the queue when list_posts was first tested (still sitting there, Substack)
  • ~4 hours from plan to production, including the template update and blog post
  • 1 recursive loop — the agent that built the feature can now use it to publish this post

Top comments (0)