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

SymptomCauseFix
process is not defined in lib buildVite targets browser by default — no Node globalsAdd build.target: 'node22' or use ssr mode
No .d.ts files generated by lib buildVite lib mode doesn't emit declarations by defaultAdd vite-plugin-dts to plugins array
MCP SDK bundled into lib outputSDK not in rollupOptions.externalAdd '@modelcontextprotocol/sdk' to external array
OAuth redirect URL mismatch in devVite dev server runs on port 5173 but MCP server expects port 3000Use proxy config to forward OAuth routes to MCP server port
SSR build imports fail at runtimePure-ESM dependency not handled by SSR external resolutionAdd package to ssr.noExternal to force bundling