Guide · Modern Build Toolchain
MCP Server SWC — fast TypeScript transpilation, @swc-node/register, .swcrc config
Three SWC behaviours cause silent problems in MCP server projects: SWC does not type-check — it strips TypeScript type annotations at Rust speed and emits JavaScript, but never validates that the types are correct; a type error that tsc would catch in 200 ms passes silently through SWC, so CI must still run tsc --noEmit separately; @swc-node/register patches the CommonJS require() hook and does not work with pure ESM projects — if your MCP server uses "type": "module" in package.json and relies on import statements, --require @swc-node/register has no effect; you must instead use --import @swc-node/register/esm (Node.js 18.19+) or the --loader flag on older versions; and SWC's decorator support requires explicit .swcrc flags — jsc.parser.decorators: true alone is insufficient; you must also set jsc.transform.legacyDecorator: true for most existing decorator patterns, and without both flags, classes decorated with @Injectable() or custom decorators are silently emitted without decorator execution.
TL;DR
Install @swc-node/register and @swc/core. For CJS MCP servers: node --require @swc-node/register src/index.ts. For ESM MCP servers: node --import @swc-node/register/esm src/index.ts. Create .swcrc with jsc.target: "es2022" and module.type: "commonjs" or "es6". Always run tsc --noEmit in CI — SWC never type-checks.
Why SWC for MCP server development
The standard TypeScript development loop — ts-node src/index.ts — starts a new tsc compilation on every run. For small MCP servers this takes 1–3 seconds; for servers in a large monorepo with shared type definitions, it can reach 10–20 seconds. SWC's Rust-based transpiler cuts that to under 50 ms, making the edit-run-test cycle nearly instant.
SWC is a transpiler, not a bundler. Its primary use in MCP server projects is as a development-time execution hook (@swc-node/register) and as a fast transformer inside other build tools (esbuild uses its own transformer; Rollup can use rollup-plugin-swc3; webpack uses swc-loader). For production, you still bundle with esbuild or Rollup — SWC is not a standalone build system.
.swcrc configuration for Node.js MCP servers
SWC reads configuration from .swcrc (JSON) in the project root. The most important fields for MCP server projects are jsc.target (which ECMAScript version to emit), jsc.parser.syntax ("typescript" to parse TS files), and module.type (whether to emit CommonJS or ES modules).
// .swcrc — for a CommonJS MCP server (package.json has no "type" or "type": "commonjs")
{
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": false, // set true if you have .tsx files (rare in MCP servers)
"decorators": false, // set true only if using decorator patterns
"dynamicImport": true // allow import() expressions
},
"target": "es2022", // emit ES2022 — supported natively by Node.js 18+
"externalHelpers": false, // true would require @swc/helpers as a runtime dep
"keepClassNames": true, // preserve class names for MCP tool handler introspection
"transform": {
"optimizer": {
"globals": {
"vars": {
"__DEV__": "false" // tree-shake dev-only code in production builds
}
}
}
}
},
"module": {
"type": "commonjs", // matches "type": "commonjs" in package.json
"strict": true, // throw on cyclic dependencies (better safety)
"lazy": false, // don't lazy-load modules (eager load is safer for MCP)
"noInterop": false // enable interop between ESM and CJS
},
"sourceMaps": true,
"inlineSourcesContent": false // keep source maps as separate .map files
}
// .swcrc — for an ESM MCP server (package.json has "type": "module")
{
"jsc": {
"parser": {
"syntax": "typescript",
"dynamicImport": true
},
"target": "es2022"
},
"module": {
"type": "es6", // emit ES modules — matches "type": "module"
// NOTE: "es6" in SWC means ES modules (ESM), not ECMAScript 2015
// despite the misleading name — use this for ESM projects
"strict": true
},
"sourceMaps": true
}
@swc-node/register for development execution
@swc-node/register installs a Node.js require hook that intercepts require('./something.ts') calls and transpiles the TypeScript on demand. The hook runs in the same process — no child process, no file watching, no intermediate build step. Start your MCP server as if it were already compiled JavaScript.
// package.json — development scripts using @swc-node/register
{
"scripts": {
// CJS project:
"dev": "node --require @swc-node/register src/index.ts",
// ESM project (Node.js 20.6+):
"dev": "node --import @swc-node/register/esm src/index.ts",
// ESM project (Node.js 18.x — uses deprecated --loader flag):
"dev": "node --loader @swc-node/register/esm-legacy src/index.ts",
// With nodemon for automatic restart on file changes:
"dev:watch": "nodemon --exec 'node --require @swc-node/register' src/index.ts --ext ts",
// Type checking (separate from execution — SWC never type-checks):
"typecheck": "tsc --noEmit",
// Production build (use esbuild or tsc for production, not SWC directly):
"build": "node build.mjs"
},
"devDependencies": {
"@swc-node/register": "^1.10.0",
"@swc/core": "^1.7.0"
}
}
The --import flag (Node.js 18.19+) is the modern way to register ESM hooks — it runs the hook before the main module loads and supports ES module import resolution. The --loader flag is deprecated but required on Node.js 18.0–18.18. Both flags support source maps natively when sourceMaps: true is set in .swcrc.
SWC in CI: type checking is still required
The most common mistake with SWC in MCP server CI pipelines is relying on SWC's success as a signal that the code is type-correct. SWC succeeds even when TypeScript types are wrong — it simply strips them. A type error that breaks a tool handler's argument schema will deploy to production undetected if tsc --noEmit is not in CI.
# .github/workflows/ci.yml — correct CI pipeline with SWC development + tsc CI
name: CI
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- run: npm ci
# Type checking — runs tsc, NOT SWC:
- name: Type check
run: npx tsc --noEmit
# This is the only step that validates TypeScript correctness.
# Never skip it just because SWC succeeded in the dev script.
# Build (can use esbuild, SWC, or tsc depending on project):
- name: Build
run: npm run build
# Tests (Vitest or Jest, both can use SWC transforms):
- name: Test
run: npm test
Running tsc --noEmit in CI alongside SWC-based development gives the best of both worlds: fast local iteration (SWC) with full type safety guarantees (tsc). The --noEmit flag makes tsc check types without writing any output files, so it's faster than a full compilation — typically 5–30 seconds depending on project size.
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
| Type errors not caught — broken code deploys | SWC never type-checks — only strips types | Add tsc --noEmit as a required CI step |
--require @swc-node/register has no effect on ESM files | CJS require() hook doesn't intercept import statements | Use --import @swc-node/register/esm for ESM projects |
| Decorators not applied at runtime | Missing jsc.transform.legacyDecorator: true in .swcrc | Add both jsc.parser.decorators: true and jsc.transform.legacyDecorator: true |
| Source maps missing from error stack traces | sourceMaps: false in .swcrc | Set sourceMaps: true and install source-map-support |
Module resolution error on .ts import paths | SWC doesn't rewrite import './foo.ts' extension in ESM mode | Use extensionless imports (import './foo') or set paths in tsconfig |