Guide · Modern Build Toolchain
MCP Server Vite — lib mode, dev server, SSR for MCP server companions
Three Vite misconceptions are common in MCP server projects: Vite's default build target is the browser, not Node.js — without build.target: 'node22' or ssr: true, Vite polyfills or strips Node.js APIs like process, Buffer, and __dirname and the MCP server runtime fails; lib mode does not generate TypeScript declaration files — Vite's lib mode (using Rollup under the hood) transpiles TypeScript but does not produce .d.ts files; you must run a separate tsc --declaration --emitDeclarationOnly step or use the vite-plugin-dts plugin; and Vite's dev server is for the frontend, not the MCP stdio transport — MCP stdio servers read from process.stdin and write to process.stdout, which the Vite HMR dev server doesn't model; Vite's dev server is useful for MCP server companion web UIs (OAuth flows, status dashboards, admin panels) but not for the MCP protocol layer itself.
TL;DR
For MCP server library packages: use build.lib with vite-plugin-dts for TypeScript declarations. For MCP server companion web UIs: use the standard Vite dev server with @vitejs/plugin-react or @vitejs/plugin-vue. For MCP servers with HTTP endpoints: use Vite's ssr build for the server bundle. Vitest shares vite.config.ts configuration, so any project using Vite also gets fast test runs for free.
When Vite fits MCP server projects
MCP servers increasingly ship with web companions: an OAuth authorization page (required by the MCP authorization spec for remote servers), a public status dashboard, an admin panel, or a self-hosted installer UI. These are standard single-page apps or server-rendered pages — Vite is the correct tool for all of them. The MCP protocol layer (stdio or SSE) runs as a separate Node.js process alongside the web UI.
The second use case is Vite lib mode: some MCP server authors publish their server as a reusable npm package. Vite lib mode wraps Rollup's library bundling under a simpler configuration interface and adds built-in TypeScript support via vite-plugin-dts.
Vite lib mode for an npm-published MCP server package
// vite.config.ts — lib mode for an MCP server library
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import { resolve } from 'path';
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'McpServerSearch', // global variable name for UMD builds (rarely needed)
formats: ['es', 'cjs'], // ESM + CommonJS dual output
fileName: (format) => `index.${format === 'es' ? 'mjs' : 'cjs'}`,
},
rollupOptions: {
// Mark @modelcontextprotocol/sdk and all other peerDependencies as external
external: [
'@modelcontextprotocol/sdk',
// Alternative: use a regex to mark all node_modules external:
// /^[^.\/]|^\.[^.\/]|^\.\.[^\/]/ (anything not starting with . or /)
],
output: {
// Preserve module structure for better tree-shaking in consumer bundles
preserveModules: true,
preserveModulesRoot: 'src',
},
},
sourcemap: true,
// Do NOT minify library output — minification breaks stack traces and
// prevents consumers from reading the source in node_modules
minify: false,
},
plugins: [
dts({
// vite-plugin-dts generates .d.ts files from your TypeScript source
// Output goes alongside the built JS (dist/index.d.ts by default)
include: ['src'],
beforeWriteFile: (filePath, content) => ({
// Remap .d.ts output paths to match the lib output paths
filePath: filePath.replace('/src/', '/'),
content,
}),
}),
],
});
// package.json — dual-format with Vite lib mode
{
"name": "@myorg/mcp-server-search",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.mjs" },
"require": { "types": "./dist/index.d.ts", "default": "./dist/index.cjs" }
}
},
"files": ["dist"],
"scripts": {
"build": "vite build",
"typecheck": "tsc --noEmit",
"dev": "vite build --watch"
},
"peerDependencies": {
"@modelcontextprotocol/sdk": "^1.4.0"
},
"devDependencies": {
"vite": "^6.0.0",
"vite-plugin-dts": "^4.0.0",
"typescript": "^5.5.0"
}
}
Vite dev server for an MCP OAuth companion UI
MCP remote servers (using SSE or Streamable HTTP transport) must implement an OAuth 2.1 authorization flow. The authorization endpoint is a web page served by your MCP server's HTTP layer. During development, Vite's dev server provides HMR for that page while the MCP protocol server runs separately on a different port.
// vite.config.ts — companion UI with proxy to MCP HTTP server
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
// Proxy MCP API calls to the Node.js MCP server
'/mcp': {
target: 'http://localhost:3000',
changeOrigin: true,
},
// Proxy OAuth endpoints to the same MCP server
'/oauth': {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
},
build: {
// Production build outputs to a directory served by Caddy / nginx
outDir: 'dist/ui',
// Use relative asset paths so the UI works at any sub-path
base: './',
},
});
# Running development: MCP server + Vite dev server concurrently
# package.json scripts using concurrently:
{
"scripts": {
"dev:server": "node --require @swc-node/register src/server.ts",
"dev:ui": "vite",
"dev": "concurrently \"npm run dev:server\" \"npm run dev:ui\""
},
"devDependencies": {
"concurrently": "^9.0.0"
}
}
Vite SSR for MCP HTTP servers with server-side rendering
MCP servers that include a server-rendered status page or documentation site can use Vite's SSR build to pre-render HTML with data from the live endpoint registry. The SSR bundle runs in Node.js alongside the MCP protocol layer.
// vite.config.ts — SSR build for a Node.js MCP HTTP server
import { defineConfig } from 'vite';
export default defineConfig({
build: {
// SSR build mode: targets Node.js, externalizes node_modules
ssr: 'src/entry-server.ts',
outDir: 'dist/server',
rollupOptions: {
// SSR mode auto-externalizes node_modules, but explicitly list
// packages that must be bundled (pure-ESM packages without CJS):
external: [],
},
},
ssr: {
// Force specific packages to be bundled even in SSR mode
// (needed for packages that don't ship CJS and aren't Node.js compatible as external)
noExternal: ['some-esm-only-package'],
},
});
Vite's SSR build sets process.env.NODE_ENV correctly and applies standard Vite plugin transforms, including TypeScript transpilation. The output is a Node.js-importable ESM module — import it from your MCP server's Express or Hono HTTP handler to render HTML server-side.
Vitest configuration sharing with vite.config.ts
Vitest is Vite's test runner (covered in the Vitest guide). When a project already has vite.config.ts, Vitest reuses it automatically — test environment settings, aliases, and plugins defined for the build also apply to tests. Only test-specific configuration (coverage, reporters, globals) needs to be added in the test block.
// vite.config.ts — combined build + test config
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
export default defineConfig({
// ... build config above ...
test: {
// Vitest-specific config — shares the rest of vite.config.ts
globals: true,
environment: 'node', // not 'jsdom' — MCP servers run in Node
coverage: {
provider: 'v8',
include: ['src/**/*.ts'],
exclude: ['src/**/*.test.ts', 'src/index.ts'],
},
// Test files that exercise tool handlers with real MCP calls:
include: ['src/**/*.test.ts', 'tests/**/*.test.ts'],
},
});
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
process is not defined in lib build | Vite targets browser by default — no Node globals | Add build.target: 'node22' or use ssr mode |
No .d.ts files generated by lib build | Vite lib mode doesn't emit declarations by default | Add vite-plugin-dts to plugins array |
| MCP SDK bundled into lib output | SDK not in rollupOptions.external | Add '@modelcontextprotocol/sdk' to external array |
| OAuth redirect URL mismatch in dev | Vite dev server runs on port 5173 but MCP server expects port 3000 | Use proxy config to forward OAuth routes to MCP server port |
| SSR build imports fail at runtime | Pure-ESM dependency not handled by SSR external resolution | Add package to ssr.noExternal to force bundling |