In-depth Next.js guides covering App Router, RSC, ISR, and deployment. Get code examples, optimization checklists, and prompts to accelerate development.
In my previous guide, we built a production-ready MCP server that allowed Claude Code to read from our blog. We could search for articles and fetch their content. This is fantastic for context—Claude can answer questions based on what I've already written.
But as I was using it, I hit a frustration point. I'd spot a typo or a sentence that needed clarification, and I'd have to:
Open my CMS (Sanity).
Find the article.
Make the edit.
Publish.
Wait for revalidation.
That's too much friction. I'm already talking to Claude; why can't I just say, "Fix that typo in the title"?
Today, we're going to expand our MCP server to support write operations. We'll implement an update_article tool and, crucially, handle Next.js cache revalidation so our changes show up instantly.
The Goal
We want to be able to tell Claude:
"Update the article 'nextjs-mcp-guide' to change the title to 'Building a Production MCP Server' and fix the typo in the first paragraph."
To do this, we need:
An update_article tool in our MCP server.
A way to write to our CMS (Sanity).
A way to tell Next.js to clear its cache for that page.
Step 1: The update_article Tool
We'll modify our existing src/app/api/mcp/[transport]/route.ts. We need to add a new tool definition that accepts the article slug and the fields we want to update.
First, make sure you have your SANITY_API_TOKEN in your .env.local (and Vercel env vars). This token needs write permissions.
typescript
// src/app/api/mcp/[transport]/route.tsimport { createClient } from'next-sanity'import { revalidatePath, revalidateTag } from'next/cache'// 👈 Don't forget this!// ... existing setup ...
server.tool(
'update_article',
'Update an existing article\'s content or metadata. Supports partial updates.',
{
slug: z.string().describe('The slug of the article to update'),
markdownContent: z.string().optional().describe('New markdown content'),
title: z.string().optional().describe('New title'),
subtitle: z.string().optional().describe('New subtitle'),
metaDescription: z.string().optional().describe('New meta description')
},
async ({ slug, markdownContent, title, subtitle, metaDescription }) => {
try {
console.log(`[MCP] update_article tool called for slug: ${slug}`)
// 1. Create a write clientconst writeClient = createClient({
projectId,
dataset,
token: process.env.SANITY_API_TOKEN, // 👈 Must have write accessapiVersion: '2023-05-03',
useCdn: false,
})
// 2. Find the document IDconst existingPost = await writeClient.fetch(
`*[_type == "post" && slug.current == $slug][0]{_id, title}`,
{ slug }
)
if (!existingPost) {
return {
content: [{ type: 'text', text: JSON.stringify({ error: `Article with slug "${slug}" not found` }) }],
isError: true
}
}
// 3. Prepare the patchconstpatch: any = {
dateModified: newDate().toISOString()
}
if (markdownContent !== undefined) patch.markdownContent = markdownContent
if (title !== undefined) patch.title = title
if (subtitle !== undefined) patch.subtitle = subtitle
if (metaDescription !== undefined) patch.metaDescription = metaDescription
// 4. Commit the patchconsole.log(`[MCP] Patching document ${existingPost._id} with:`, Object.keys(patch))
const result = await writeClient.patch(existingPost._id).set(patch).commit()
// 5. Revalidate (The Magic Part ✨)console.log(`[MCP] Revalidating paths for slug: ${slug}`)
revalidatePath(`/blog/${slug}`)
revalidatePath('/blog')
revalidateTag('post')
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
message: 'Article updated successfully and revalidated',
updatedFields: Object.keys(patch),
article: {
slug,
title: result.title
}
}, null, 2)
}]
}
} catch (error) {
// ... error handling ...
}
}
)
Step 2: The Importance of Revalidation
The code above looks straightforward, but step 5 is critical.
Next.js is aggressive about caching. If you update the content in Sanity but don't tell Next.js, your site will continue serving the old static HTML until the cache naturally expires (which could be days).
Because our MCP server is running inside our Next.js application (via mcp-handler), we have direct access to next/cache.
revalidatePath('/blog/[slug]'): Clears the cache for the specific article page.
revalidatePath('/blog'): Clears the blog listing page (so the new title/subtitle shows up there).
revalidateTag('post'): Clears any data fetches tagged with 'post' (useful if you use unstable_cache).
This is much simpler than setting up a webhook handler for Sanity just to handle these manual edits. We know exactly what changed, so we can revalidate it immediately.
Step 3: Testing It Out
Now for the fun part. Restart your dev server (npm run dev) and Claude Code.
Try a more complex request, like adding an update note to an article:
"Check the article 'cloudflare-outage'. Add a note to the beginning saying: 'UPDATE: The issue has been resolved as of 10:15 AM.'"
Claude will:
Call get_article_content to read the current text.
Prepend the update note to the markdown.
Call update_article with the new content.
Receive the success message confirming revalidation.
If you refresh your local browser (or production site, if you deployed this), you'll see the change instantly. This is much faster than the manual CMS workflow.
Security Note
With great power comes great responsibility. You are giving an AI agent write access to your database.
Keep your SANITY_API_TOKEN secret. Never commit it to git.
Scope your MCP server. If you're using mcp-handler, it's protected by your Next.js auth or firewall rules in production.
Review changes. Claude is smart, but it can hallucinate. Always verify the changes it makes, especially for large content updates.
What's Next?
Continue the Series
Your MCP server now has both read and write capabilities. Here's where to go next:
Part 3: OAuth for MCP Servers — Essential security before deploying write tools to production