├── .cursor └── mcp.json ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .vscode ├── mcp.json └── settings.json ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── alias.mjs ├── build.config.ts ├── eslint.config.js ├── package.json ├── packages ├── nuxt-mcp │ ├── README.md │ ├── build.config.ts │ ├── package.json │ ├── playground │ │ ├── app.vue │ │ ├── nuxt.config.ts │ │ ├── package.json │ │ ├── server │ │ │ └── tsconfig.json │ │ └── tsconfig.json │ └── src │ │ ├── module.ts │ │ ├── prompts │ │ └── basic.ts │ │ ├── tools │ │ ├── runtime.ts │ │ └── scaffold.ts │ │ └── types.ts └── vite-plugin-mcp │ ├── README.md │ ├── build.config.ts │ ├── package.json │ ├── playground │ ├── index.html │ ├── package.json │ ├── src │ │ ├── counter.ts │ │ ├── main.ts │ │ └── vite-env.d.ts │ └── vite.config.ts │ └── src │ ├── connect.ts │ ├── index.ts │ ├── server.ts │ └── types.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── test ├── exports.test.ts ├── exports │ ├── nuxt-mcp.yaml │ └── vite-plugin-mcp.yaml └── index.test.ts ├── tsconfig.json └── vitest.config.ts /.cursor/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "vite": { 4 | "url": "http://localhost:5200/__mcp/sse" 5 | }, 6 | "nuxt": { 7 | "url": "http://localhost:4000/__mcp/sse" 8 | }, 9 | "nuxt-docs": { 10 | "url": "https://mcp.nuxt.com/sse" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [antfu] 2 | opencollective: antfu 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: pnpm/action-setup@v4 18 | with: 19 | run_install: false 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: lts/* 23 | cache: pnpm 24 | 25 | - run: pnpm i -g @antfu/ni 26 | - run: nci 27 | - run: nr lint 28 | - run: nr typecheck 29 | 30 | test: 31 | runs-on: ${{ matrix.os }} 32 | 33 | strategy: 34 | matrix: 35 | node: [lts/*] 36 | os: [ubuntu-latest, windows-latest, macos-latest] 37 | fail-fast: false 38 | 39 | steps: 40 | - uses: actions/checkout@v4 41 | - uses: pnpm/action-setup@v4 42 | with: 43 | run_install: false 44 | - name: Set node ${{ matrix.node }} 45 | uses: actions/setup-node@v4 46 | with: 47 | node-version: ${{ matrix.node }} 48 | cache: pnpm 49 | 50 | - run: pnpm i -g @antfu/ni 51 | - run: nci 52 | - run: nr build 53 | - run: nr test 54 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | id-token: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | tags: 10 | - 'v*' 11 | 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - uses: pnpm/action-setup@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: lts/* 23 | registry-url: https://registry.npmjs.org/ 24 | 25 | - run: pnpm dlx changelogithub 26 | env: 27 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 28 | 29 | # # Uncomment the following lines to publish to npm on CI 30 | # 31 | # - run: pnpm install 32 | # - run: pnpm publish --no-git-checks -r --access public 33 | # env: 34 | # NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 35 | # NPM_CONFIG_PROVENANCE: true 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .DS_Store 3 | .idea 4 | *.log 5 | *.tgz 6 | coverage 7 | dist 8 | lib-cov 9 | logs 10 | node_modules 11 | temp 12 | .nuxt 13 | .output 14 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | shell-emulator=true 3 | -------------------------------------------------------------------------------- /.vscode/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "servers": { 3 | "nuxt-docs": { 4 | "type": "sse", 5 | "url": "https://mcp.nuxt.com/sse" 6 | }, 7 | "vite": { 8 | "type": "sse", 9 | "url": "http://localhost:5200/__mcp/sse" 10 | }, 11 | "nuxt": { 12 | "type": "sse", 13 | "url": "http://localhost:4000/__mcp/sse" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Disable the default formatter, use eslint instead 3 | "prettier.enable": false, 4 | "editor.formatOnSave": false, 5 | 6 | // Auto fix 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": "explicit", 9 | "source.organizeImports": "never" 10 | }, 11 | 12 | // Silent the stylistic rules in you IDE, but still auto fix them 13 | "eslint.rules.customizations": [ 14 | { "rule": "style/*", "severity": "off" }, 15 | { "rule": "*-indent", "severity": "off" }, 16 | { "rule": "*-spacing", "severity": "off" }, 17 | { "rule": "*-spaces", "severity": "off" }, 18 | { "rule": "*-order", "severity": "off" }, 19 | { "rule": "*-dangle", "severity": "off" }, 20 | { "rule": "*-newline", "severity": "off" }, 21 | { "rule": "*quotes", "severity": "off" }, 22 | { "rule": "*semi", "severity": "off" } 23 | ], 24 | 25 | // Enable eslint for all supported languages 26 | "eslint.validate": [ 27 | "javascript", 28 | "javascriptreact", 29 | "typescript", 30 | "typescriptreact", 31 | "vue", 32 | "html", 33 | "markdown", 34 | "json", 35 | "jsonc", 36 | "yaml" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please refer to https://github.com/antfu/contribute 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025-PRESENT Anthony Fu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nuxt-mcp / vite-plugin-mcp 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![bundle][bundle-src]][bundle-href] 6 | [![JSDocs][jsdocs-src]][jsdocs-href] 7 | [![License][license-src]][license-href] 8 | 9 | MCP server helping models to understand your Vite/Nuxt app better. 10 | 11 | This monorepo contains two packages: 12 | 13 | - [`nuxt-mcp`](./packages/nuxt-mcp) - A Nuxt module for adding MCP support to your Nuxt app. 14 | - [`vite-plugin-mcp`](./packages/vite-plugin-mcp) - A Vite plugin for adding MCP support to your Vite app. 15 | 16 | > [!IMPORTANT] 17 | > Experimental. Use with caution. 18 | 19 | ## Sponsors 20 | 21 |

22 | 23 | 24 | 25 |

26 | 27 | ## License 28 | 29 | [MIT](./LICENSE) License © [Anthony Fu](https://github.com/antfu) 30 | 31 | 32 | 33 | [npm-version-src]: https://img.shields.io/npm/v/nuxt-mcp?style=flat&colorA=080f12&colorB=1fa669 34 | [npm-version-href]: https://npmjs.com/package/nuxt-mcp 35 | [npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-mcp?style=flat&colorA=080f12&colorB=1fa669 36 | [npm-downloads-href]: https://npmjs.com/package/nuxt-mcp 37 | [bundle-src]: https://img.shields.io/bundlephobia/minzip/nuxt-mcp?style=flat&colorA=080f12&colorB=1fa669&label=minzip 38 | [bundle-href]: https://bundlephobia.com/result?p=nuxt-mcp 39 | [license-src]: https://img.shields.io/github/license/antfu/nuxt-mcp.svg?style=flat&colorA=080f12&colorB=1fa669 40 | [license-href]: https://github.com/antfu/nuxt-mcp/blob/main/LICENSE 41 | [jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669 42 | [jsdocs-href]: https://www.jsdocs.io/package/nuxt-mcp 43 | -------------------------------------------------------------------------------- /alias.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | 3 | const r = path => fileURLToPath(new URL(path, import.meta.url)) 4 | 5 | export default { 6 | 'vite-plugin-mcp': r('./packages/vite-plugin-mcp/src/index.ts'), 7 | } 8 | -------------------------------------------------------------------------------- /build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | 3 | export default defineBuildConfig({ 4 | entries: [ 5 | 'src/index', 6 | ], 7 | declaration: 'node16', 8 | clean: true, 9 | rollup: { 10 | inlineDependencies: [ 11 | '@antfu/utils', 12 | ], 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import antfu from '@antfu/eslint-config' 3 | 4 | export default antfu( 5 | { 6 | type: 'lib', 7 | pnpm: true, 8 | }, 9 | ) 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "version": "0.2.2", 4 | "private": true, 5 | "packageManager": "pnpm@10.10.0", 6 | "scripts": { 7 | "build": "pnpm -r run build", 8 | "dev": "pnpm -r run dev", 9 | "play": "pnpm -C packages/nuxt-mcp play", 10 | "lint": "eslint", 11 | "release": "bumpp -r && pnpm -r publish", 12 | "test": "vitest", 13 | "inspect": "npx @modelcontextprotocol/inspector", 14 | "typecheck": "tsc --noEmit", 15 | "prepare": "simple-git-hooks && nr -r dev:prepare" 16 | }, 17 | "devDependencies": { 18 | "@antfu/eslint-config": "catalog:cli", 19 | "@antfu/ni": "catalog:cli", 20 | "@antfu/utils": "catalog:utils", 21 | "@modelcontextprotocol/inspector": "catalog:cli", 22 | "@modelcontextprotocol/sdk": "catalog:prod", 23 | "@types/node": "catalog:types", 24 | "bumpp": "catalog:cli", 25 | "eslint": "catalog:cli", 26 | "h3": "catalog:prod", 27 | "lint-staged": "catalog:cli", 28 | "nodemon": "catalog:cli", 29 | "simple-git-hooks": "catalog:cli", 30 | "tinyexec": "catalog:utils", 31 | "tsx": "catalog:cli", 32 | "typescript": "catalog:cli", 33 | "unbuild": "catalog:cli", 34 | "vite": "catalog:cli", 35 | "vitest": "catalog:testing", 36 | "vitest-package-exports": "catalog:testing", 37 | "yaml": "catalog:testing", 38 | "zod": "catalog:prod" 39 | }, 40 | "simple-git-hooks": { 41 | "pre-commit": "pnpm i --frozen-lockfile --ignore-scripts --offline && npx lint-staged" 42 | }, 43 | "lint-staged": { 44 | "*": "eslint --fix" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/nuxt-mcp/README.md: -------------------------------------------------------------------------------- 1 | # nuxt-mcp 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![bundle][bundle-src]][bundle-href] 6 | [![JSDocs][jsdocs-src]][jsdocs-href] 7 | [![License][license-src]][license-href] 8 | 9 | MCP server helping models to understand your Nuxt app better. 10 | 11 | > [!IMPORTANT] 12 | > Experimental. Use with caution. 13 | 14 | ```ts 15 | // nuxt.config.ts 16 | 17 | export default defineNuxtConfig({ 18 | modules: ['nuxt-mcp'], 19 | }) 20 | ``` 21 | 22 | Then the MCP server will be available at `http://localhost:3000/__mcp/sse`. 23 | 24 | > 💡 When using VSCode, Cursor, Windsurf, the module will automatically update the config files for you. 25 | 26 | ## Module Hooks 27 | 28 | For other modules to provide additional information to MCP, you can use the `mcp:setup` hook. 29 | 30 | ```ts 31 | // src/module.ts 32 | 33 | export default defineNuxtModule({ 34 | meta: { 35 | name: 'my-module', 36 | }, 37 | async setup(options, nuxt) { 38 | nuxt.hook('mcp:setup', ({ mcp }) => { 39 | // Setup your MCP tools here 40 | // For example 41 | mcp.tool('get-nuxt-root', 'Get the Nuxt root path', {}, async () => { 42 | return { 43 | content: [{ 44 | type: 'text', 45 | text: nuxt.options.rootDir, 46 | }], 47 | } 48 | }) 49 | }) 50 | }, 51 | }) 52 | ``` 53 | 54 | ## Sponsors 55 | 56 |

57 | 58 | 59 | 60 |

61 | 62 | ## License 63 | 64 | [MIT](./LICENSE) License © [Anthony Fu](https://github.com/antfu) 65 | 66 | 67 | 68 | [npm-version-src]: https://img.shields.io/npm/v/nuxt-mcp?style=flat&colorA=080f12&colorB=1fa669 69 | [npm-version-href]: https://npmjs.com/package/nuxt-mcp 70 | [npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-mcp?style=flat&colorA=080f12&colorB=1fa669 71 | [npm-downloads-href]: https://npmjs.com/package/nuxt-mcp 72 | [bundle-src]: https://img.shields.io/bundlephobia/minzip/nuxt-mcp?style=flat&colorA=080f12&colorB=1fa669&label=minzip 73 | [bundle-href]: https://bundlephobia.com/result?p=nuxt-mcp 74 | [license-src]: https://img.shields.io/github/license/antfu/nuxt-mcp.svg?style=flat&colorA=080f12&colorB=1fa669 75 | [license-href]: https://github.com/antfu/nuxt-mcp/blob/main/LICENSE 76 | [jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669 77 | [jsdocs-href]: https://www.jsdocs.io/package/nuxt-mcp 78 | -------------------------------------------------------------------------------- /packages/nuxt-mcp/build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | 3 | export default defineBuildConfig({ 4 | entries: [], 5 | }) 6 | -------------------------------------------------------------------------------- /packages/nuxt-mcp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-mcp", 3 | "type": "module", 4 | "version": "0.2.2", 5 | "description": "MCP server helping models to understand your Nuxt app better.", 6 | "license": "MIT", 7 | "homepage": "https://github.com/antfu/nuxt-mcp#readme", 8 | "repository": "antfu/nuxt-mcp", 9 | "bugs": "https://github.com/antfu/nuxt-mcp/issues", 10 | "keywords": [ 11 | "nuxt", 12 | "mcp", 13 | "modelcontextprotocol" 14 | ], 15 | "sideEffects": false, 16 | "exports": { 17 | ".": "./dist/module.mjs" 18 | }, 19 | "main": "./dist/module.mjs", 20 | "module": "./dist/module.mjs", 21 | "types": "./dist/types.d.mts", 22 | "files": [ 23 | "dist" 24 | ], 25 | "scripts": { 26 | "build": "nuxt-module-build build", 27 | "prepublishOnly": "nr build", 28 | "play": "DEBUG=nuxt:mcp:server,vite:mcp:server PORT=4000 nuxi dev playground", 29 | "play:build": "nuxi build playground", 30 | "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground" 31 | }, 32 | "peerDependencies": { 33 | "nitropack": "^2 || ^3", 34 | "nuxi": ">=3", 35 | "nuxt": ">=3.5.0", 36 | "vite": ">=6" 37 | }, 38 | "dependencies": { 39 | "@modelcontextprotocol/sdk": "catalog:prod", 40 | "@nuxt/kit": "catalog:nuxt", 41 | "ansis": "catalog:prod", 42 | "async-cache-dedupe": "catalog:prod", 43 | "citty": "catalog:prod", 44 | "debug": "catalog:prod", 45 | "pathe": "catalog:prod", 46 | "unimport": "catalog:prod", 47 | "vite-plugin-mcp": "workspace:*", 48 | "zod": "catalog:prod" 49 | }, 50 | "devDependencies": { 51 | "@nuxt/devtools": "catalog:nuxt", 52 | "@nuxt/eslint-config": "catalog:nuxt", 53 | "@nuxt/module-builder": "catalog:nuxt", 54 | "@nuxt/schema": "catalog:nuxt", 55 | "@nuxt/test-utils": "catalog:nuxt", 56 | "@types/debug": "catalog:types", 57 | "@types/node": "catalog:types", 58 | "eslint": "catalog:cli", 59 | "nitropack": "catalog:nuxt", 60 | "nuxi": "catalog:nuxt", 61 | "nuxt": "catalog:nuxt", 62 | "typescript": "catalog:cli", 63 | "vitest": "catalog:testing", 64 | "vue-tsc": "catalog:cli" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/nuxt-mcp/playground/app.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /packages/nuxt-mcp/playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from 'nuxt/config' 2 | 3 | export default defineNuxtConfig({ 4 | modules: ['../src/module'], 5 | devtools: { enabled: true }, 6 | compatibilityDate: '2025-03-11', 7 | debug: false, 8 | }) 9 | -------------------------------------------------------------------------------- /packages/nuxt-mcp/playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-module-playground", 3 | "type": "module", 4 | "private": true, 5 | "dependencies": { 6 | "nuxt": "catalog:nuxt" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/nuxt-mcp/playground/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/nuxt-mcp/playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/nuxt-mcp/src/module.ts: -------------------------------------------------------------------------------- 1 | import type { Nitro } from 'nitropack' 2 | import type { Unimport } from 'unimport' 3 | import type { ViteMcpOptions } from 'vite-plugin-mcp' 4 | import type { McpToolContext } from './types' 5 | import { addVitePlugin, defineNuxtModule } from '@nuxt/kit' 6 | import { ViteMcp } from 'vite-plugin-mcp' 7 | import { promptNuxtBasic } from './prompts/basic' 8 | import { toolsNuxtRuntime } from './tools/runtime' 9 | import { toolsScaffold } from './tools/scaffold' 10 | 11 | export interface ModuleOptions extends ViteMcpOptions { 12 | /** 13 | * Includes the online Nuxt MCP server from https://mcp.nuxt.com/sse 14 | * 15 | * This MCP would provide information about the Nuxt ecosystem, including 16 | * the latest documentation, available modules, etc. 17 | * 18 | * @default true 19 | */ 20 | includeNuxtDocsMcp?: boolean 21 | } 22 | 23 | export interface ModuleHooks { 24 | 'mcp:setup': (context: McpToolContext) => void 25 | } 26 | 27 | export default defineNuxtModule({ 28 | meta: { 29 | name: 'nuxt-mcp', 30 | configKey: 'mcp', 31 | }, 32 | defaults: { 33 | includeNuxtDocsMcp: true, 34 | }, 35 | async setup(options, nuxt) { 36 | const unimport = promiseWithResolve() 37 | const nitro = promiseWithResolve() 38 | 39 | nuxt.hook('imports:context', (_unimport) => { 40 | unimport.resolve(_unimport) 41 | }) 42 | nuxt.hook('nitro:init', (_nitro) => { 43 | nitro.resolve(_nitro) 44 | }) 45 | 46 | addVitePlugin(ViteMcp({ 47 | updateConfigServerName: 'nuxt', 48 | ...options, 49 | updateConfigAdditionalServers: [ 50 | ...options.updateConfigAdditionalServers || [], 51 | ...( 52 | options.includeNuxtDocsMcp 53 | ? [{ 54 | name: 'nuxt-docs', 55 | url: 'https://mcp.nuxt.com/sse', 56 | }] 57 | : []), 58 | ], 59 | port: nuxt.options.devServer.port, 60 | async mcpServerSetup(mcp, vite) { 61 | await options.mcpServerSetup?.(mcp, vite) 62 | 63 | const context: McpToolContext = { 64 | unimport: unimport.promise, 65 | nitro: nitro.promise, 66 | nuxt, 67 | vite, 68 | mcp, 69 | } 70 | 71 | promptNuxtBasic(context) 72 | toolsNuxtRuntime(context) 73 | toolsScaffold(context) 74 | 75 | // eslint-disable-next-line ts/ban-ts-comment 76 | // @ts-ignore skip type infer 77 | await nuxt.callHook('mcp:setup', context) 78 | }, 79 | }), { client: true, server: false }) 80 | }, 81 | }) 82 | 83 | function promiseWithResolve(): { promise: Promise, resolve: (value: T) => void } { 84 | let resolve: (value: T) => void = undefined! 85 | const promise = new Promise((_resolve) => { 86 | resolve = _resolve 87 | }) 88 | return { promise, resolve } 89 | } 90 | 91 | declare module 'nuxt/schema' { 92 | interface NuxtHooks { 93 | 'mcp:setup': (ctx: McpToolContext) => void | Promise 94 | } 95 | } 96 | 97 | declare module '@nuxt/schema' { 98 | interface NuxtHooks { 99 | 'mcp:setup': (ctx: McpToolContext) => void | Promise 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /packages/nuxt-mcp/src/prompts/basic.ts: -------------------------------------------------------------------------------- 1 | import type { McpToolContext } from '../types' 2 | 3 | export function promptNuxtBasic({ mcp, nuxt }: McpToolContext): void { 4 | mcp.prompt('nuxt-basic', () => { 5 | const lines: string[] = [] 6 | 7 | lines.push(` 8 | You are a professional developer specializing in Nuxt and Vue. 9 | 10 | This nuxt project is configured with following structure: 11 | - Root directory: ${nuxt.options.rootDir} 12 | - App directory: ${nuxt.options.appDir} 13 | - Source code directory: ${nuxt.options.srcDir} 14 | - Server directory: ${nuxt.options.serverDir} 15 | 16 | `) 17 | 18 | // TODO: components, typescript, tailwind, unocss 19 | 20 | if (nuxt.options.ssr) { 21 | lines.push(` 22 | ## SSR 23 | 24 | This is a Nuxt 3 application with SSR enabled. Components should be isomorphic and can be executed on both server or client side. 25 | 26 | In scenarios where you need different logic for server and client, use \`import.meta.client\` and \`import.meta.server\` 27 | to branch the code, and use dynamic imports if needed. 28 | `) 29 | } 30 | else { 31 | lines.push(` 32 | ## CSR 33 | 34 | This is a Nuxt 3 application with SSR disabled. While components are primarily rendered on the client side, still try to make the code as isomorphic as possible to be future-proof. 35 | `) 36 | } 37 | 38 | if (nuxt.options.imports.autoImport) { 39 | lines.push(` 40 | ## Auto-imports 41 | 42 | This Nuxt project is configured have auto-imports enabled. 43 | 44 | For example, Vue apis (ref, computed, watch, etc.) are auto-imported, so you can directly use them in your code without import statements. 45 | 46 | You can find the full auto-import items with tool \`list-nuxt-auto-imports-items\`. 47 | `) 48 | } 49 | else { 50 | lines.push(` 51 | ## No auto-imports 52 | 53 | This Nuxt project is configured have auto-imports disabled. 54 | 55 | You can still reference the registered entries with tool \`list-nuxt-auto-imports-items\`. 56 | But you need to always manually import the usages when modifying the code, either directly import the item from their source, 57 | or use explicit \`import { xxx } from '#imports'\` to import any items from the registry. 58 | `) 59 | } 60 | 61 | // Referene prompts from https://github.com/antfu/nuxt-mcp/issues/2 by @DannyVogel 62 | // lines.push(` 63 | // - This is a Nuxt 3 application using the Composition API with 82 | // - Always handle all data fetching states in templates: 83 | // 88 | 89 | // - Follow component best practices: 90 | 91 | // - Create small, focused components for specific tasks 92 | // - Use defineModel for two-way binding between parent and child components 93 | // - Use props for passing data down to child components 94 | // - Use emits for communicating events up to parent components 95 | // - Use composables for shared state and logic across components 96 | // - Use Pinia for global state management 97 | // - Consider provide/inject only for specific cases like theme providers or deeply nested component trees 98 | 99 | // - Always structure Vue Single File Components in this exact order: 100 | 101 | // 1. 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/vite-plugin-mcp/playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "type": "module", 4 | "version": "0.2.2", 5 | "private": true, 6 | "scripts": { 7 | "dev": "DEBUG=vite:mcp:server vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/vite-plugin-mcp/playground/src/counter.ts: -------------------------------------------------------------------------------- 1 | export function setupCounter(element: HTMLButtonElement): void { 2 | let counter = 0 3 | const setCounter = (count: number): void => { 4 | counter = count 5 | element.innerHTML = `count is ${counter}` 6 | } 7 | element.addEventListener('click', () => setCounter(counter + 1)) 8 | setCounter(0) 9 | } 10 | -------------------------------------------------------------------------------- /packages/vite-plugin-mcp/playground/src/main.ts: -------------------------------------------------------------------------------- 1 | import { setupCounter } from './counter.ts' 2 | 3 | document.querySelector('#app')!.innerHTML = ` 4 |
5 |

Vite + TypeScript

6 |
7 | 8 |
9 |

10 | Click on the Vite and TypeScript logos to learn more 11 |

12 |
13 | ` 14 | 15 | setupCounter(document.querySelector('#counter')!) 16 | -------------------------------------------------------------------------------- /packages/vite-plugin-mcp/playground/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/vite-plugin-mcp/playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { ViteMcp } from '../src' 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | ViteMcp(), 7 | ], 8 | server: { 9 | port: 5200, 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /packages/vite-plugin-mcp/src/connect.ts: -------------------------------------------------------------------------------- 1 | import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' 2 | import type { ViteDevServer } from 'vite' 3 | import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js' 4 | import DEBUG from 'debug' 5 | 6 | const debug = DEBUG('vite:mcp:server') 7 | 8 | export async function setupRoutes(base: string, server: McpServer, vite: ViteDevServer): Promise { 9 | const transports = new Map() 10 | 11 | vite.middlewares.use(`${base}/sse`, async (req, res) => { 12 | const transport = new SSEServerTransport(`${base}/messages`, res) 13 | transports.set(transport.sessionId, transport) 14 | debug('SSE Connected %s', transport.sessionId) 15 | res.on('close', () => { 16 | transports.delete(transport.sessionId) 17 | }) 18 | await server.connect(transport) 19 | }) 20 | 21 | vite.middlewares.use(`${base}/messages`, async (req, res) => { 22 | if (req.method !== 'POST') { 23 | res.statusCode = 405 24 | res.end('Method Not Allowed') 25 | return 26 | } 27 | 28 | const query = new URLSearchParams(req.url?.split('?').pop() || '') 29 | const clientId = query.get('sessionId') 30 | 31 | if (!clientId || typeof clientId !== 'string') { 32 | res.statusCode = 400 33 | res.end('Bad Request') 34 | return 35 | } 36 | 37 | const transport = transports.get(clientId) 38 | if (!transport) { 39 | res.statusCode = 404 40 | res.end('Not Found') 41 | return 42 | } 43 | 44 | debug('Message from %s', clientId) 45 | await transport.handlePostMessage(req, res) 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /packages/vite-plugin-mcp/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin, ViteDevServer } from 'vite' 2 | import type { ViteMcpOptions } from './types' 3 | import { existsSync } from 'node:fs' 4 | import fs from 'node:fs/promises' 5 | import { homedir } from 'node:os' 6 | import c from 'ansis' 7 | import { join } from 'pathe' 8 | import { searchForWorkspaceRoot } from 'vite' 9 | import { setupRoutes } from './connect' 10 | 11 | export * from './types' 12 | 13 | const CONSOLE_LOG_PREFIX = c.cyan.bold`[MCP] ` 14 | 15 | export function ViteMcp(options: ViteMcpOptions = {}): Plugin { 16 | const { 17 | printUrl = true, 18 | mcpServer = (vite: ViteDevServer) => import('./server').then(m => m.createMcpServerDefault(options, vite)), 19 | } = options 20 | 21 | const mcpRoute = options.mcpRouteRoot ?? options.mcpPath ?? '/__mcp' 22 | 23 | return { 24 | name: 'vite-plugin-mcp', 25 | async configureServer(vite) { 26 | let mcp = await mcpServer(vite) 27 | mcp = await options.mcpServerSetup?.(mcp, vite) || mcp 28 | await setupRoutes(mcpRoute, mcp, vite) 29 | 30 | const port = vite.config.server.port 31 | const root = searchForWorkspaceRoot(vite.config.root) 32 | 33 | const protocol = vite.config.server.https ? 'https' : 'http' 34 | const sseUrl = `${protocol}://${options.host || 'localhost'}:${options.port || port}${mcpRoute}/sse` 35 | 36 | if (printUrl) { 37 | // eslint-disable-next-line no-console 38 | console.log(`${c.cyan` ➜ MCP: `}${c.gray(`Mcp server is running at ${c.green(sseUrl)}`)}`) 39 | } 40 | await updateConfigs(root, sseUrl, options, vite) 41 | }, 42 | } 43 | } 44 | 45 | async function updateConfigs( 46 | root: string, 47 | sseUrl: string, 48 | options: ViteMcpOptions, 49 | vite: ViteDevServer, 50 | ): Promise { 51 | const { 52 | updateConfig = 'auto', 53 | updateConfigServerName = 'vite', 54 | updateConfigAdditionalServers = [], 55 | } = options 56 | 57 | if (updateConfig === false) 58 | return 59 | 60 | const configs = updateConfig === 'auto' 61 | ? [ 62 | existsSync(join(root, '.cursor')) ? 'cursor' as const : null, 63 | existsSync(join(root, '.vscode')) ? 'vscode' as const : null, 64 | existsSync(join(homedir(), '.codeium', 'windsurf')) ? 'windsurf' as const : null, 65 | ].filter(x => x !== null) 66 | : Array.isArray(updateConfig) 67 | ? updateConfig 68 | : [] 69 | 70 | // Cursor 71 | if (configs.includes('cursor')) { 72 | await fs.mkdir(join(root, '.cursor'), { recursive: true }) 73 | const mcp = existsSync(join(root, '.cursor/mcp.json')) 74 | ? JSON.parse(await fs.readFile(join(root, '.cursor/mcp.json'), 'utf-8') || '{}') 75 | : {} 76 | mcp.mcpServers ||= {} 77 | mcp.mcpServers[updateConfigServerName || 'vite'] = { url: sseUrl } 78 | for (const server of updateConfigAdditionalServers) { 79 | mcp.mcpServers[server.name] = { url: server.url } 80 | } 81 | await fs.writeFile(join(root, '.cursor/mcp.json'), `${JSON.stringify(mcp, null, 2)}\n`) 82 | vite.config.logger.info(`${CONSOLE_LOG_PREFIX}${c.gray(`Updated config file ${join(root, '.cursor/mcp.json')}`)}`) 83 | } 84 | 85 | // VSCode 86 | if (configs.includes('vscode')) { 87 | await fs.mkdir(join(root, '.vscode'), { recursive: true }) 88 | const mcp = existsSync(join(root, '.vscode/mcp.json')) 89 | ? JSON.parse(await fs.readFile(join(root, '.vscode/mcp.json'), 'utf-8') || '{}') 90 | : {} 91 | mcp.servers ||= {} 92 | mcp.servers[updateConfigServerName || 'vite'] = { 93 | type: 'sse', 94 | url: sseUrl, 95 | } 96 | for (const server of updateConfigAdditionalServers) { 97 | mcp.servers[server.name] = { 98 | type: 'sse', 99 | url: server.url, 100 | } 101 | } 102 | await fs.writeFile(join(root, '.vscode/mcp.json'), `${JSON.stringify(mcp, null, 2)}\n`) 103 | vite.config.logger.info(`${CONSOLE_LOG_PREFIX}${c.gray(`Updated config file ${join(root, '.vscode/mcp.json')}`)}`) 104 | } 105 | 106 | // Windsurf 107 | if (configs.includes('windsurf')) { 108 | const windsurfDir = join(homedir(), '.codeium', 'windsurf') 109 | const windsurfConfigPath = join(windsurfDir, 'mcp_config.json') 110 | try { 111 | await fs.mkdir(windsurfDir, { recursive: true }) 112 | const config = existsSync(windsurfConfigPath) 113 | ? JSON.parse(await fs.readFile(windsurfConfigPath, 'utf-8').catch(() => '{}') || '{}') 114 | : {} 115 | config.mcpServers ||= {} 116 | config.mcpServers[updateConfigServerName || 'vite'] = { serverUrl: sseUrl } 117 | for (const server of updateConfigAdditionalServers) { 118 | config.mcpServers[server.name] = { serverUrl: server.url } 119 | } 120 | await fs.writeFile(windsurfConfigPath, `${JSON.stringify(config, null, 2)}\n`) 121 | vite.config.logger.info(`${CONSOLE_LOG_PREFIX}${c.gray(`Updated config file ${windsurfConfigPath}`)}`) 122 | } 123 | catch (e) { 124 | vite.config.logger.error(`${CONSOLE_LOG_PREFIX}${c.red(`Failed to update ${windsurfConfigPath}`)}${e}`) 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /packages/vite-plugin-mcp/src/server.ts: -------------------------------------------------------------------------------- 1 | import type { ViteDevServer } from 'vite' 2 | import type { ViteMcpOptions } from './types' 3 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' 4 | import { z } from 'zod' 5 | import { version } from '../package.json' 6 | 7 | export function createMcpServerDefault( 8 | options: ViteMcpOptions, 9 | vite: ViteDevServer, 10 | ): McpServer { 11 | const server = new McpServer( 12 | { 13 | name: 'vite', 14 | version, 15 | ...options.mcpServerInfo, 16 | }, 17 | ) 18 | 19 | server.tool( 20 | 'get-vite-config', 21 | 'Get the Vite config digest, including the root, resolve, plugins, and environment names.', 22 | {}, 23 | async () => ({ 24 | content: [{ 25 | type: 'text', 26 | text: JSON.stringify({ 27 | root: vite.config.root, 28 | resolve: vite.config.resolve, 29 | plugins: vite.config.plugins.map(p => p.name).filter(Boolean), 30 | environmentNames: Object.keys(vite.environments), 31 | }), 32 | }], 33 | }), 34 | ) 35 | 36 | server.tool( 37 | 'get-vite-module-info', 38 | 'Get graph information of a module, including importers, imported modules, and compiled result.', 39 | { 40 | filepath: z.string() 41 | .describe('The absolute filepath of the module'), 42 | }, 43 | async ({ filepath }) => { 44 | const records: any[] = [] 45 | Object.entries(vite.environments).forEach(([key, env]) => { 46 | const mods = env.moduleGraph.getModulesByFile(filepath) 47 | for (const mod of mods || []) { 48 | records.push({ 49 | environment: key, 50 | id: mod.id, 51 | url: mod.url, 52 | importers: Array.from(mod.importers), 53 | importedModules: Array.from(mod.importedModules), 54 | meta: mod.meta, 55 | lastHMRTimestamp: mod.lastHMRTimestamp, 56 | transformResult: mod.transformResult, 57 | }) 58 | } 59 | }) 60 | return { 61 | content: [{ 62 | type: 'text', 63 | text: JSON.stringify(records), 64 | }], 65 | } 66 | }, 67 | ) 68 | 69 | return server 70 | } 71 | -------------------------------------------------------------------------------- /packages/vite-plugin-mcp/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Awaitable } from '@antfu/utils' 2 | import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' 3 | import type { Implementation as McpServerInfo } from '@modelcontextprotocol/sdk/types.js' 4 | import type { ViteDevServer } from 'vite' 5 | 6 | export type SupportedUpdateConfigType = 'cursor' | 'vscode' | 'windsurf' 7 | 8 | export interface UpdateConfigAdditionalServer { 9 | name: string 10 | url: string 11 | } 12 | 13 | export type MaybeArray = T | T[] 14 | 15 | export type { McpServer } 16 | 17 | export interface ViteMcpOptions { 18 | /** 19 | * The host to listen on, default is `localhost` 20 | */ 21 | host?: string 22 | 23 | /** 24 | * The port to listen on, default is the port of the Vite dev server 25 | */ 26 | port?: number 27 | 28 | /** 29 | * Print the MCP server URL in the console 30 | * 31 | * @default true 32 | */ 33 | printUrl?: boolean 34 | 35 | /** 36 | * The MCP server info. Ingored when `mcpServer` is provided 37 | */ 38 | mcpServerInfo?: McpServerInfo 39 | 40 | /** 41 | * Custom MCP server, when this is provided, the built-in MCP tools will be ignored 42 | */ 43 | mcpServer?: (viteServer: ViteDevServer) => Awaitable 44 | 45 | /** 46 | * Setup the MCP server, this is called when the MCP server is created 47 | * You may also return a new MCP server to replace the default one 48 | */ 49 | mcpServerSetup?: (server: McpServer, viteServer: ViteDevServer) => Awaitable 50 | 51 | /** 52 | * The root route to the MCP server, default is `/__mcp` 53 | */ 54 | mcpRouteRoot?: string 55 | 56 | /** 57 | * The config types to update 58 | * 59 | * - `auto` - Automatically update the config files if they exists 60 | * - `cursor` - Update the cursor config file `.cursor/mcp.json` 61 | * - `vscode` - Update the VSCode config file `.vscode/settings.json` 62 | * - `windsurf` - Update the Windsurf config file `~/.codeium/windsurf/mcp_config.json` 63 | * 64 | * @default 'auto' 65 | */ 66 | updateConfig?: 'auto' | false | MaybeArray 67 | 68 | /** 69 | * Additional servers to update the config files 70 | */ 71 | updateConfigAdditionalServers?: UpdateConfigAdditionalServer[] 72 | 73 | /** 74 | * The name of the MCP server when updating the config files 75 | * 76 | * @default 'vite' 77 | */ 78 | updateConfigServerName?: string 79 | 80 | // --------- DEPRECATED --------- 81 | 82 | /** 83 | * @deprecated Use `mcpRouteRoot` instead 84 | */ 85 | mcpPath?: string 86 | } 87 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - playground 3 | - docs 4 | - packages/* 5 | - examples/* 6 | catalogs: 7 | cli: 8 | '@antfu/eslint-config': ^4.13.0 9 | '@antfu/ni': ^24.3.0 10 | '@modelcontextprotocol/inspector': ^0.11.0 11 | bumpp: ^10.1.0 12 | eslint: ^9.26.0 13 | lint-staged: ^15.5.2 14 | nodemon: ^3.1.10 15 | simple-git-hooks: ^2.13.0 16 | tsx: ^4.19.4 17 | typescript: ~5.8.3 18 | unbuild: ^3.5.0 19 | vite: ^6.3.5 20 | vue-tsc: ^2.2.10 21 | nuxt: 22 | '@nuxt/devtools': ^2.4.0 23 | '@nuxt/eslint-config': ^1.3.0 24 | '@nuxt/kit': ^3.17.2 25 | '@nuxt/module-builder': ^1.0.1 26 | '@nuxt/schema': ^3.17.2 27 | '@nuxt/test-utils': ^3.18.0 28 | nitropack: ^2.11.11 29 | nuxi: ^3.25.0 30 | nuxt: ^3.17.2 31 | prod: 32 | '@modelcontextprotocol/sdk': ^1.11.1 33 | ansis: ^3.17.0 34 | async-cache-dedupe: ^2.2.0 35 | citty: ^0.1.6 36 | debug: ^4.4.0 37 | h3: ^1.15.3 38 | pathe: ^2.0.3 39 | unimport: ^5.0.1 40 | zod: ^3.24.4 41 | testing: 42 | vitest: ^3.1.3 43 | vitest-package-exports: ^0.1.1 44 | yaml: ^2.7.1 45 | types: 46 | '@types/debug': ^4.1.12 47 | '@types/node': latest 48 | utils: 49 | '@antfu/utils': ^9.2.0 50 | tinyexec: ^1.0.1 51 | onlyBuiltDependencies: 52 | - esbuild 53 | - simple-git-hooks 54 | - unrs-resolver 55 | -------------------------------------------------------------------------------- /test/exports.test.ts: -------------------------------------------------------------------------------- 1 | import { x } from 'tinyexec' 2 | import { describe, expect, it } from 'vitest' 3 | import { getPackageExportsManifest } from 'vitest-package-exports' 4 | import yaml from 'yaml' 5 | 6 | describe('exports-snapshot', async () => { 7 | const packages: { name: string, path: string, private?: boolean }[] = JSON.parse( 8 | await x('pnpm', ['ls', '--only-projects', '-r', '--json']).then(r => r.stdout), 9 | ) 10 | 11 | for (const pkg of packages) { 12 | if (pkg.private) 13 | continue 14 | it(`${pkg.name}`, async () => { 15 | const manifest = await getPackageExportsManifest({ 16 | importMode: 'src', 17 | cwd: pkg.path, 18 | }) 19 | await expect(yaml.stringify(manifest.exports)) 20 | .toMatchFileSnapshot(`./exports/${pkg.name}.yaml`) 21 | }) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /test/exports/nuxt-mcp.yaml: -------------------------------------------------------------------------------- 1 | .: 2 | default: function 3 | -------------------------------------------------------------------------------- /test/exports/vite-plugin-mcp.yaml: -------------------------------------------------------------------------------- 1 | .: 2 | ViteMcp: function 3 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | 3 | describe('should', () => { 4 | it('exported', () => { 5 | expect(1).toEqual(1) 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["ESNext", "DOM"], 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "paths": { 8 | "vite-plugin-mcp": ["./packages/vite-plugin-mcp/src/index.ts"] 9 | }, 10 | "resolveJsonModule": true, 11 | "allowImportingTsExtensions": true, 12 | "strict": true, 13 | "strictNullChecks": true, 14 | "noEmit": true, 15 | "esModuleInterop": true, 16 | "verbatimModuleSyntax": true, 17 | "skipDefaultLibCheck": true, 18 | "skipLibCheck": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | server: { 6 | deps: { 7 | inline: ['vitest-package-exports'], 8 | }, 9 | }, 10 | }, 11 | }) 12 | --------------------------------------------------------------------------------