Article·  

Building an MCP Server for Nuxt

How we built the Nuxt MCP server to enable AI assistants to access our documentation through structured data and composable tools.
Hugo Richard

Hugo Richard

@hugorcd__

Sébastien Chopin

Sébastien Chopin

@Atinux

AI assistants are becoming an increasingly important part of the developer experience. To help them provide accurate, up-to-date information about Nuxt, we built an MCP server that exposes our documentation, blog posts, and deployment guides in a structured way. Here's how we did it, and how you can build your own.

What is MCP and why did we build it?

The Model Context Protocol (MCP) is an open standard that enables AI assistants to securely access data and tools. Think of it as an API specifically designed for AI assistants: rather than returning HTML or generic JSON, it provides structured, semantic data that LLMs can easily understand and use.

MCP defines three main primitives:

  • Resources: Data that provides context to language models, uniquely identified by URIs (like documentation pages or blog posts)
  • Tools: Enable AI models to interact with external systems and perform operations (like searching or API calls)
  • Prompts: Reusable prompt templates with arguments that can be invoked by users

Why MCP over RAG?

We've observed that AI assistants using MCP servers provide significantly better responses than traditional RAG (Retrieval-Augmented Generation) approaches:

  • Structured data in, structured data out: Tools accept well-defined parameters and return typed data that prevents hallucinations
  • Composable tools: AI assistants can chain tools together, using the output of one tool as input for another (e.g., search for a topic, then fetch the full content)
  • Faster and more accurate: No need to process and chunk large documents at query time
  • Always up-to-date: Direct access to your content layer without reindexing
  • Context-aware navigation: The AI can intelligently navigate relationships between content

Both Nuxt and Nuxt UI now have MCP servers with similar architectures, making it easier for AI assistants to help developers with these frameworks.

Technical architecture

Our MCP server is built directly into nuxt.com as a server route, leveraging Nuxt's full-stack capabilities:

nuxt.com/
├── server/
│   ├── routes/
│   │   └── mcp.ts  # Main MCP server
│   └── api/
│       └── mcp/
│           ├── list-documentation-pages.get.ts
│           ├── get-documentation-page.get.ts
│           ├── search-content.get.ts
│           └── ... (other endpoints)
└── content/
    ├── blog/
    ├── deploy/
    └── ... (Nuxt Content files)

The architecture is straightforward. The MCP Server Route (server/routes/mcp.ts) handles the MCP protocol, the API Endpoints (server/api/mcp/*.ts) query Nuxt Content, and the HTTP Transport enables real-time communication with AI assistants.

Implementation deep dive

Server setup

The foundation is the @modelcontextprotocol/sdk package. We create an MCP server instance and configure it:

server/routes/mcp.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'

function createServer() {
  const server = new McpServer({
    name: 'nuxt-com',
    version: '1.0.0'
  })

  // Register resources, tools, and prompts here...
  
  return server
}

Resources: Context for language models

Resources provide data and context that language models can use. Each resource is identified by a URI. Here's how we expose all documentation pages:

server/routes/mcp.ts
server.registerResource(
  'nuxt-documentation-pages',
  'resource://nuxt-com/documentation-pages',
  {
    title: 'Nuxt Documentation Pages',
    description: 'Complete list of available Nuxt documentation pages'
  },
  async (uri) => {
    const result = await $fetch('/api/mcp/list-documentation-pages', {
      query: { version: '4.x' }
    })
    return {
      contents: [{
        uri: uri.href,
        mimeType: 'application/json',
        text: JSON.stringify(result, null, 2)
      }]
    }
  }
)

The API endpoint uses Nuxt Content's queryCollection to fetch data efficiently:

server/api/mcp/list-documentation-pages.get.ts
import { queryCollection } from '@nuxt/content/server'
import { z } from 'zod'

const querySchema = z.object({
  version: z.enum(['3.x', '4.x', 'all']).optional().default('4.x')
})

export default defineCachedEventHandler(async (event) => {
  const { version } = await getValidatedQuery(event, querySchema.parse)

  const allDocs = await queryCollection(event, 'docsv4')
    .select('title', 'path', 'description')
    .all()

  return allDocs.map(doc => ({
    title: doc.title,
    path: doc.path,
    description: doc.description,
    version: doc.path.includes('/docs/4.x') ? '4.x' : '3.x',
    url: `https://nuxt.com${doc.path}`
  }))
}, {
  maxAge: 60 * 60, // Cache for 1 hour
  getKey: (event) => `mcp-documentation-pages-${getQuery(event).version || '4.x'}`
})

Notice the caching strategy, this ensures fast responses while keeping content fresh.

Tools: Operations for AI models

Tools enable language models to interact with external systems by accepting parameters and performing operations. Our search_content tool demonstrates parameter validation with Zod:

server/routes/mcp.ts
server.tool(
  'search_content',
  'Searches across all Nuxt content including documentation, blog posts, and deployment guides',
  {
    query: z.string().describe('Search query'),
    type: z.enum(['docs', 'blog', 'deploy', 'all']).optional(),
    version: z.enum(['3.x', '4.x', 'all']).optional()
  },
  async (params) => {
    const result = await $fetch('/api/mcp/search-content', { query: params })
    return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
  }
)

The search implementation uses Nuxt Content's query capabilities:

server/api/mcp/search-content.get.ts
export default defineEventHandler(async (event) => {
  const { query: searchQuery, type, version } = await getValidatedQuery(event, querySchema.parse)

  const results = []

  if (type === 'docs' || type === 'all') {
    const docs = await queryCollection(event, 'docsv4')
      .where('title', 'LIKE', `%${searchQuery}%`)
      .select('title', 'path', 'description')
      .limit(20)
      .all()

    results.push(...docs.map(doc => ({
      type: 'documentation',
      title: doc.title,
      path: doc.path,
      description: doc.description,
      url: `https://nuxt.com${doc.path}`
    })))
  }

  // Similar logic for blog and deploy content...

  return results
})

Prompts: Reusable templates

Prompts are reusable templates with arguments that users can invoke. They return a conversation format that guides the AI through specific workflows:

server/routes/mcp.ts
server.registerPrompt(
  'find_documentation_for_topic',
  {
    title: 'Find Documentation for Topic',
    description: 'Find the best Nuxt documentation for a specific topic',
    argsSchema: {
      topic: z.string().describe('What you want to learn about')
    }
  },
  async ({ topic }) => {
    const searchResults = await $fetch('/api/mcp/search-content', {
      query: { query: topic, type: 'docs' }
    })
    
    return {
      messages: [{
        role: 'user',
        content: {
          type: 'text',
          text: `Help me find documentation for: "${topic}". Results: ${JSON.stringify(searchResults, null, 2)}`
        }
      }]
    }
  }
)

Prompts differ from tools in that they are user-invoked and return conversation messages, while tools are model-controlled and return structured data.

HTTP transport layer

The final piece is handling the HTTP communication using Streamable HTTP transport:

server/routes/mcp.ts
export default defineEventHandler(async (event) => {
  // Redirect browsers to documentation
  if (getHeader(event, 'accept')?.includes('text/html')) {
    return sendRedirect(event, '/docs/guide/ai/mcp')
  }

  const server = createServer()

  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined
  })

  // Clean up on connection close
  event.node.res.on('close', () => {
    transport.close()
    server.close()
  })

  await server.connect(transport)

  const body = await readBody(event)
  await transport.handleRequest(event.node.req, event.node.res, body)
})

This handler does three important things:

  1. Redirects browser traffic to our documentation
  2. Manages server and transport lifecycle
  3. Processes MCP protocol requests using HTTP POST/GET

The StreamableHTTPServerTransport can optionally use Server-Sent Events (SSE) for streaming responses when multiple messages need to be sent back to the client, but it also supports simple JSON responses for basic request-response patterns.

Building your own MCP server

Ready to build an MCP server for your own application? Here's a simplified guide:

1. Install dependencies

npm install @modelcontextprotocol/sdk zod

2. Create a server route

Create server/routes/mcp.ts in your Nuxt project:

server/routes/mcp.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'

function createServer() {
  const server = new McpServer({
    name: 'my-app',
    version: '1.0.0'
  })

  // Add your resources, tools, and prompts

  return server
}

export default defineEventHandler(async (event) => {
  const server = createServer()
  const transport = new StreamableHTTPServerTransport()
  
  await server.connect(transport)
  const body = await readBody(event)
  await transport.handleRequest(event.node.req, event.node.res, body)
})

3. Register your first resource

server/routes/mcp.ts
server.registerResource(
  'my-content',
  'resource://my-app/content',
  {
    title: 'My Content',
    description: 'All my app content'
  },
  async (uri) => {
    const data = await $fetch('/api/my-content')
    return {
      contents: [{
        uri: uri.href,
        mimeType: 'application/json',
        text: JSON.stringify(data, null, 2)
      }]
    }
  }
)

4. Add a search tool

server/routes/mcp.ts
server.tool(
  'search',
  'Search through my content',
  {
    query: z.string().describe('Search query')
  },
  async ({ query }) => {
    const results = await $fetch('/api/search', { query: { q: query } })
    return { content: [{ type: 'text', text: JSON.stringify(results) }] }
  }
)

5. Create supporting API endpoints

Use Nuxt Content, a database, or any data source to power your API endpoints:

server/api/my-content.get.ts
import { queryCollection } from '@nuxt/content/server'

export default defineEventHandler(async (event) => {
  const content = await queryCollection(event, 'my-collection').all()
  return content
})

That's it! Your MCP server is now accessible at https://your-domain.com/mcp.

Get started with the Nuxt MCP server

Ready to experience the power of MCP with Nuxt? Our server is already live and provides access to all Nuxt documentation, blog posts, and deployment guides.

Quick install for Cursor

The easiest way to get started is with Cursor's one-click installation:

Install Nuxt MCP Server in Cursor

Other AI assistants

The Nuxt MCP server works with Claude Desktop, Windsurf, Visual Studio Code, ChatGPT, and many other MCP-compatible AI assistants. For complete setup instructions for all platforms, check out our MCP documentation.

We encourage you to build MCP servers for your own applications. Whether it's documentation, API references, or domain-specific knowledge, MCP makes it easy for AI assistants to provide accurate, helpful information to your users.

The complete source code for our MCP server is available on GitHub in the server/routes/mcp.ts file and server/api/mcp/ directory. Feel free to use it as inspiration for your own implementation!