Expanding Your Next.js MCP Server — Editing & Revalidation

Add write capabilities to your MCP server and handle Next.js cache revalidation for instant updates.

·Matija Žiberna·
Expanding Your Next.js MCP Server — Editing & Revalidation

⚡ Next.js Implementation Guides

In-depth Next.js guides covering App Router, RSC, ISR, and deployment. Get code examples, optimization checklists, and prompts to accelerate development.

No spam. Unsubscribe anytime.

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:

  1. Open my CMS (Sanity).
  2. Find the article.
  3. Make the edit.
  4. Publish.
  5. 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:

  1. An update_article tool in our MCP server.
  2. A way to write to our CMS (Sanity).
  3. 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.

// src/app/api/mcp/[transport]/route.ts
import { 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 client
            const writeClient = createClient({
                projectId,
                dataset,
                token: process.env.SANITY_API_TOKEN, // 👈 Must have write access
                apiVersion: '2023-05-03',
                useCdn: false,
            })

            // 2. Find the document ID
            const 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 patch
            const patch: any = {
                dateModified: new Date().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 patch
            console.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:

  1. Call get_article_content to read the current text.
  2. Prepend the update note to the markdown.
  3. Call update_article with the new content.
  4. 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?

We've gone from reading to writing. Now our MCP server is a true CMS interface. You could expand this further:

  • Create new articles: Add a create_article tool.
  • Manage images: Add tools to upload images to Sanity.
  • SEO optimization: Create a tool that analyzes content and updates keywords automatically.

The possibilities are endless when your AI coding assistant is directly connected to your application's core logic.

2

Frequently Asked Questions

Comments

Leave a Comment

Your email will not be published

10-2000 characters

• Comments are automatically approved and will appear immediately

• Your name and email will be saved for future comments

• Be respectful and constructive in your feedback

• No spam, self-promotion, or off-topic content

Matija Žiberna
Matija Žiberna
Full-stack developer, co-founder

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.