---
title: "Build an MCP server with Nuxt MCP Toolkit · vuejs.school"
description: "Give an AI assistant two tools from a Nuxt todo app with the three-week-old MCP Toolkit module, test them in DevTools and connect Cursor or VS Code."
canonical_url: "https://vuejs.school/community/blog/build-an-mcp-server-with-nuxt-mcp-toolkit"
meta:
  "og:description": "Give an AI assistant two tools from a Nuxt todo app with the three-week-old MCP Toolkit module, test them in DevTools and connect Cursor or VS Code."
  "og:image": "https://vuejs.school/_og/d/a_~QnVpbGQgYW4gTUNQIHNlcnZlciB3aXRoIE51eHQgTUNQIFRvb2xraXQgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_Build%2Ban%2BMCP%2Bserver%2Bwith%2BNuxt%2BMCP%2BToolkit%2Cdescription_~R2l2ZSBhbiBBSSBhc3Npc3RhbnQgdHdvIHRvb2xzIGZyb20gYSBOdXh0IHRvZG8gYXBwIHdpdGggdGhlIHRocmVlLXdlZWstb2xkIE1DUCBUb29sa2l0IG1vZHVsZSwgdGVzdCB0aGVtIGluIERldlRvb2xzIGFuZCBjb25uZWN0IEN1cnNvciBvciBWUyBDb2RlLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_sand%2Cp_Ii9jb21tdW5pdHkvYmxvZy9idWlsZC1hbi1tY3Atc2VydmVyLXdpdGgtbnV4dC1tY3AtdG9vbGtpdCI%2Cs_OtHmd9N3Qir2ceR2.png"
  "og:image:alt": "Build an MCP server with Nuxt MCP Toolkit · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Build an MCP server with Nuxt MCP Toolkit · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~QnVpbGQgYW4gTUNQIHNlcnZlciB3aXRoIE51eHQgTUNQIFRvb2xraXQgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_Build%2Ban%2BMCP%2Bserver%2Bwith%2BNuxt%2BMCP%2BToolkit%2Cdescription_~R2l2ZSBhbiBBSSBhc3Npc3RhbnQgdHdvIHRvb2xzIGZyb20gYSBOdXh0IHRvZG8gYXBwIHdpdGggdGhlIHRocmVlLXdlZWstb2xkIE1DUCBUb29sa2l0IG1vZHVsZSwgdGVzdCB0aGVtIGluIERldlRvb2xzIGFuZCBjb25uZWN0IEN1cnNvciBvciBWUyBDb2RlLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_sand%2Cp_Ii9jb21tdW5pdHkvYmxvZy9idWlsZC1hbi1tY3Atc2VydmVyLXdpdGgtbnV4dC1tY3AtdG9vbGtpdCI%2Cs_OtHmd9N3Qir2ceR2.png"
  "twitter:image:alt": "Build an MCP server with Nuxt MCP Toolkit · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## What MCP is and what the toolkit adds

The [Model Context Protocol](https://modelcontextprotocol.io) is an open standard for connecting AI assistants to data and tools. A server offers three kinds of things:

- Tools: functions the assistant may call.
- Resources: data it may read by URI.
- Prompts: reusable message templates.

Clients that speak the protocol, such as Cursor, VS Code and the desktop chat apps, can use a server's tools without any code written for that server.

The Nuxt team runs a server of their own at `https://nuxt.com/mcp` and described how they built it in [Building an MCP Server for Nuxt](https://nuxt.com/blog/building-nuxt-mcp) on 13 November: tools that list and fetch documentation pages, resources for the docs and blog, and prompts that find the right page for a topic.

The module packages that pattern. You add it to `nuxt.config.ts` and put files in `server/mcp/tools/`, `server/mcp/resources/` and `server/mcp/prompts/`. The module discovers them, validates tool and prompt arguments with Zod, and serves everything at `/mcp` from the same Nitro server that already runs your API routes. There is no second process to deploy, but there must be a server: under `nuxt generate` the module switches itself off with a warning.

## Install the module

One command installs the package and adds it to the modules list:

```
npx nuxt module add mcp-toolkit
```

To do it by hand, install `@nuxtjs/mcp-toolkit` and its peer dependency `zod`, which must be Zod 4, then register the module. The [module's docs](https://mcp-toolkit.nuxt.dev) ask for Nuxt 3 or 4 and Node 18 or newer.

Then give the server a name in `nuxt.config.ts`; the module sends it to clients when they connect. DevTools stays on for the inspector you will open later:

*nuxt.config.ts*

```
export default defineNuxtConfig({
  modules: ['@nuxtjs/mcp-toolkit'],
  devtools: { enabled: true },
  mcp: {
    name: 'Todo app',
  },
})
```

The `mcp` block has six options, and `name` is the only one worth setting on day one. The other five, with their defaults:

- `route`, the endpoint path: `/mcp`.
- `dir`, the folder under `server/` that the module scans: `mcp`.
- `version`, reported to clients when they connect: `1.0.0`.
- `browserRedirect`, where a person who opens the endpoint in a browser is sent: `/`.
- `enabled`, whether the server runs at all: `true`.

To check the route, start `npm run dev` and open `http://localhost:3000/mcp` in a browser. Being sent to the home page tells you the route is live. Until the first tool exists, the terminal also warns that no MCP definitions were found.

## Keep the todos in a server utility

To keep the focus on MCP rather than databases, the todos live in memory in a server utility that your API routes can share; the list resets when the server restarts. In the Nuxt 4 layout `server/` stays at the project root, next to `app/`:

*server/utils/todos.ts*

```
export type Todo = { id: number; title: string; done: boolean }

const todos: Todo[] = [
  { id: 1, title: 'Write the lesson on slots', done: true },
  { id: 2, title: 'Record the quiz questions', done: false },
]

export function listTodos(filter: 'all' | 'open' | 'done' = 'all'): Todo[] {
  if (filter === 'all') return todos
  return todos.filter(todo => (filter === 'done' ? todo.done : !todo.done))
}

export function addTodo(title: string): Todo {
  const todo = { id: Date.now(), title, done: false }
  todos.push(todo)
  return todo
}
```

In a real app these two functions would call your database. They would then return promises, so the tools would `await` them; nothing else in the MCP files changes.

## Write a read-only tool: list-todos

Each file in `server/mcp/tools/` exports one tool. `defineMcpTool` is auto-imported in server files, as are the result helpers used below. `z` is not, so import it from `zod`:

*server/mcp/tools/list-todos.ts*

```
import { z } from 'zod'
import { listTodos } from '../../utils/todos'

export default defineMcpTool({
  name: 'list-todos',
  description: 'List the todos, optionally only the open or only the done ones. Use this before answering any question about what is planned, open or finished.',
  inputSchema: {
    filter: z.enum(['all', 'open', 'done']).default('all').describe('Which todos to return'),
  },
  annotations: {
    readOnlyHint: true,
  },
  handler: async ({ filter }) => jsonResult(listTodos(filter)),
})
```

What each field does:

- `name` is the identifier the assistant calls. Leave it out and the module derives it from the file name, so `list-todos.ts` gets `list-todos` anyway.
- `description` is what the assistant reads to decide whether to call the tool. Write it for a reader who has never seen your app.
- `inputSchema` is a plain object with one Zod schema per parameter. The arguments are checked against it before the handler runs, and the handler sees `filter` typed as `'all' | 'open' | 'done'`. Because of `.default('all')`, the assistant may leave it out. The `.describe()` text travels with the schema to the client, so the assistant knows what each parameter means.
- `readOnlyHint: true` tells the client that calling the tool changes nothing, so it may skip asking the user for confirmation.
- `handler` receives the validated arguments and must return a result object with a `content` array. `jsonResult()` builds one from any value by serialising it as pretty-printed JSON; `textResult()` does the same for a string.

For a tool that reads something expensive, add `cache: '1h'` (or any duration `ms` understands) and the module caches the response through Nitro's cache, keyed by the arguments.

## Write a tool that changes data: add-todo

The second tool writes. Zod refuses an empty title before the handler runs, the handler refuses a duplicate with an error result, and the annotations describe the kind of change it makes:

*server/mcp/tools/add-todo.ts*

```
import { z } from 'zod'
import { addTodo, listTodos } from '../../utils/todos'

export default defineMcpTool({
  name: 'add-todo',
  description: 'Add a new todo with the given title. Call this only when the user asks to add, create or remember a task.',
  inputSchema: {
    title: z.string().trim().min(1).max(120).describe('What needs doing, in a few words'),
  },
  annotations: {
    readOnlyHint: false,
    destructiveHint: false,
    idempotentHint: false,
  },
  handler: async ({ title }) => {
    const exists = listTodos().some(todo => todo.title.toLowerCase() === title.toLowerCase())
    if (exists) return errorResult(`There is already a todo called "${title}".`)
    const todo = addTodo(title)
    return textResult(`Added todo ${todo.id}: ${todo.title}`)
  },
})
```

`errorResult()` returns the message with `isError: true`, which tells the assistant the call ran but did not do what was asked, so it can tell the user or try something else. The toolkit docs return an error result from a `catch` block in the same way. If an exception escapes instead, the MCP SDK under the module still turns it into an error result, but with the raw exception message (a database error, say) rather than a sentence you chose.

The annotations are hints a client may use to decide when to ask the user first. `destructiveHint: false` says the tool only adds data. Leave it out and the hint counts as `true`, which tells the client the tool may delete or overwrite data. `idempotentHint: false` is the default and promises nothing about repeated calls. Here the duplicate check makes a second call with the same title harmless, so `true` would also be accurate.

The description is the part people get wrong. Here is the mistake and the fix, on the same tool:

*server/mcp/tools/add-todo.ts**Breaks***

```
description: 'Todo tool',
```

Nothing crashes. The assistant simply has no idea when to use the tool, so it calls it when the user asks what is open, or never calls it at all. The fix is to say what it does and when to use it:

*server/mcp/tools/add-todo.ts**Works***

```
description: 'Add a new todo with the given title. Call this only when the user asks to add, create or remember a task.',
```

Most of the nuxt.com tools go further and add a "WHEN NOT TO USE" line to the description. Treat the description as documentation for a colleague who will only ever see the tool list.

## Test the tools in the MCP Inspector

Start (or restart) the dev server, and the terminal reports that `/mcp` is enabled with 2 tools. Then use the official MCP Inspector, which the module adds to Nuxt DevTools and connects to your endpoint with no setup:

1. Open Nuxt DevTools in the running app, find the MCP Inspector tab in the Server section and click Launch Inspector. The first launch downloads the inspector with `npx`, so give it a few seconds.
2. Browse the tool list, pick `list-todos`, set `filter` to `open` and run it. The response shows the JSON from `jsonResult()`.
3. Run `add-todo` with a title, then `list-todos` again. The new item is there.
4. Run `add-todo` with the same title, and you get the error result.

The inspector keeps a history of every request and response, which is the fastest way to see what an assistant will see. It uses port 6274 for its interface and 6277 for its proxy. If either is taken, start the dev server with `CLIENT_PORT` and `SERVER_PORT` set to free ports.

## Connect Cursor or VS Code

Clients connect over HTTP, so the only thing they need is the URL. For Cursor, add the server to `~/.cursor/mcp.json`:

*\~/.cursor/mcp.json*

```
{
  "mcpServers": {
    "todo-app": {
      "url": "http://localhost:3000/mcp"
    }
  }
}
```

For VS Code the file is `.vscode/mcp.json` in the project, with a slightly different shape:

*.vscode/mcp.json*

```
{
  "servers": {
    "todo-app": {
      "type": "http",
      "url": "http://localhost:3000/mcp"
    }
  }
}
```

Reload the editor and ask "what is still open on my list?" The assistant should call `list-todos` with `filter: "open"` and answer from the result.

The module also serves `/mcp/deeplink`, a redirect that installs the server in Cursor or VS Code in one click, and registers an `InstallButton` component you can put in your own docs to link to it.

## Protect the endpoint before you deploy

The module has no authentication of its own; its six options do not include one. Whatever a tool can do, anyone who can reach `/mcp` can do. For a public site that means read-only tools over data that is public anyway, as on nuxt.com. A tool that writes, like `add-todo`, belongs behind your own server middleware that checks a session or a token on the `/mcp` route, or in an app that is only reachable on your own network.

**Check it stuck**3 questions from the MCP servers and AI-assisted development quiz.

[**Practice now**](https://vuejs.school/learn/ai/mcp-servers-and-ai-assisted-development?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 7 min read**RAG in a Nuxt app: embeddings, retrieval and a chatbot that knows your docs**](https://vuejs.school/community/blog/rag-in-a-nuxt-app-a-chatbot-that-knows-your-docs) [Tutorial · 6 min read**Tools and structured output with the AI SDK 7**](https://vuejs.school/community/blog/tools-and-structured-output-with-the-ai-sdk-7)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/build-an-mcp-server-with-nuxt-mcp-toolkit#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-12-16","description":"Give an AI assistant two tools from a Nuxt todo app with the three-week-old MCP Toolkit module, test them in DevTools and connect Cursor or VS Code.","name":"Build an MCP server with Nuxt MCP Toolkit","url":"https://vuejs.school/community/blog/build-an-mcp-server-with-nuxt-mcp-toolkit","breadcrumb":{"@id":"https://vuejs.school/community/blog/build-an-mcp-server-with-nuxt-mcp-toolkit#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/build-an-mcp-server-with-nuxt-mcp-toolkit"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/build-an-mcp-server-with-nuxt-mcp-toolkit#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-12-16","description":"Give an AI assistant two tools from a Nuxt todo app with the three-week-old MCP Toolkit module, test them in DevTools and connect Cursor or VS Code.","headline":"Build an MCP server with Nuxt MCP Toolkit","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~QnVpbGQgYW4gTUNQIHNlcnZlciB3aXRoIE51eHQgTUNQIFRvb2xraXQgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_Build%2Ban%2BMCP%2Bserver%2Bwith%2BNuxt%2BMCP%2BToolkit%2Cdescription_~R2l2ZSBhbiBBSSBhc3Npc3RhbnQgdHdvIHRvb2xzIGZyb20gYSBOdXh0IHRvZG8gYXBwIHdpdGggdGhlIHRocmVlLXdlZWstb2xkIE1DUCBUb29sa2l0IG1vZHVsZSwgdGVzdCB0aGVtIGluIERldlRvb2xzIGFuZCBjb25uZWN0IEN1cnNvciBvciBWUyBDb2RlLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_sand%2Cp_Ii9jb21tdW5pdHkvYmxvZy9idWlsZC1hbi1tY3Atc2VydmVyLXdpdGgtbnV4dC1tY3AtdG9vbGtpdCI%2Cs_OtHmd9N3Qir2ceR2.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/build-an-mcp-server-with-nuxt-mcp-toolkit#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/build-an-mcp-server-with-nuxt-mcp-toolkit#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/build-an-mcp-server-with-nuxt-mcp-toolkit#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Build an MCP server with Nuxt MCP Toolkit","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~QnVpbGQgYW4gTUNQIHNlcnZlciB3aXRoIE51eHQgTUNQIFRvb2xraXQgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_Build%2Ban%2BMCP%2Bserver%2Bwith%2BNuxt%2BMCP%2BToolkit%2Cdescription_~R2l2ZSBhbiBBSSBhc3Npc3RhbnQgdHdvIHRvb2xzIGZyb20gYSBOdXh0IHRvZG8gYXBwIHdpdGggdGhlIHRocmVlLXdlZWstb2xkIE1DUCBUb29sa2l0IG1vZHVsZSwgdGVzdCB0aGVtIGluIERldlRvb2xzIGFuZCBjb25uZWN0IEN1cnNvciBvciBWUyBDb2RlLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_sand%2Cp_Ii9jb21tdW5pdHkvYmxvZy9idWlsZC1hbi1tY3Atc2VydmVyLXdpdGgtbnV4dC1tY3AtdG9vbGtpdCI%2Cs_OtHmd9N3Qir2ceR2.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~QnVpbGQgYW4gTUNQIHNlcnZlciB3aXRoIE51eHQgTUNQIFRvb2xraXQgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_Build%2Ban%2BMCP%2Bserver%2Bwith%2BNuxt%2BMCP%2BToolkit%2Cdescription_~R2l2ZSBhbiBBSSBhc3Npc3RhbnQgdHdvIHRvb2xzIGZyb20gYSBOdXh0IHRvZG8gYXBwIHdpdGggdGhlIHRocmVlLXdlZWstb2xkIE1DUCBUb29sa2l0IG1vZHVsZSwgdGVzdCB0aGVtIGluIERldlRvb2xzIGFuZCBjb25uZWN0IEN1cnNvciBvciBWUyBDb2RlLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_sand%2Cp_Ii9jb21tdW5pdHkvYmxvZy9idWlsZC1hbi1tY3Atc2VydmVyLXdpdGgtbnV4dC1tY3AtdG9vbGtpdCI%2Cs_OtHmd9N3Qir2ceR2.png"}]}
```