Guide · Code Quality & Release Tooling

MCP Server Prettier — eslint-config-prettier, opinionated formatting, pre-commit

Two Prettier mistakes slow down MCP server teams: using eslint-plugin-prettier instead of eslint-config-prettiereslint-plugin-prettier runs Prettier as an ESLint rule on every lint pass, which doubles lint time and produces error messages that say "Replace · with ↵·" instead of just formatting the file; the Prettier team actively discourages this plugin and recommends running Prettier as a separate command; formatting generated or large files that should be excluded — Prettier will attempt to format every file it finds unless you provide a .prettierignore, and running it over node_modules/, dist/, or auto-generated JSON fixtures in your MCP test suite adds seconds to every format pass and produces noisy diffs in PRs.

TL;DR

Install prettier and eslint-config-prettier (NOT eslint-plugin-prettier). Add prettierConfig as the last entry in eslint.config.js to disable ESLint rules that conflict with Prettier's formatting. Run Prettier separately via npm run format and via lint-staged on pre-commit (formats only staged files). Add a .prettierignore that excludes dist/, node_modules/, and any generated fixtures or JSON files.

Installing and configuring Prettier with ESLint v9

In ESLint v9's flat config, eslint-config-prettier is applied by spreading prettierConfig as the last item in the config array. "Last" matters because Prettier's config disables conflicting formatting rules — if another config added later re-enables them, Prettier and ESLint will fight over formatting on every save.

# Install Prettier and the ESLint config that disables conflicting rules
npm install -D prettier eslint-config-prettier

# Do NOT install eslint-plugin-prettier — it runs Prettier inside ESLint (slow)
# The Prettier docs explicitly recommend against it for most setups
// eslint.config.js — add prettierConfig LAST so it wins over any conflicting rules
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import pluginN from 'eslint-plugin-n';
import prettierConfig from 'eslint-config-prettier';  // ← import, not require

export default tseslint.config(
  { ignores: ['dist/**', 'node_modules/**', '*.d.ts'] },

  eslint.configs.recommended,
  ...tseslint.configs.recommendedTypeChecked,

  {
    files: ['**/*.ts'],
    plugins: { n: pluginN },
    languageOptions: {
      parserOptions: { project: true, tsconfigRootDir: import.meta.dirname },
    },
    rules: {
      '@typescript-eslint/no-floating-promises': 'error',
      '@typescript-eslint/no-misused-promises': 'error',
      'n/no-process-exit': 'error',
    },
  },

  // MUST be last — disables ESLint rules that conflict with Prettier's formatting
  // Specifically turns off: max-len, no-tabs, quotes, semi, indent, and ~20 others
  prettierConfig,
);

Add the Prettier scripts to package.json. The --check flag is for CI — it exits non-zero if any file needs reformatting without writing changes. Use --write locally to actually format files.

// package.json
{
  "scripts": {
    "format":       "prettier --write .",
    "format:check": "prettier --check .",
    "lint":         "eslint src/",
    "check":        "npm run lint && npm run format:check && npm run typecheck"
  }
}

Prettier configuration for TypeScript MCP servers

Prettier's defaults are reasonable, but a few settings matter specifically for TypeScript MCP server projects. The key decision is semicolons and quotes — pick one convention in .prettierrc and enforce it across the whole codebase. For TypeScript projects, trailing commas in multi-line expressions are strongly recommended because they produce cleaner diffs when adding a new parameter to a tool's input schema.

// .prettierrc  (JSON format — Prettier reads this automatically)
{
  "semi": true,
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 100,
  "tabWidth": 2,
  "useTabs": false,
  "bracketSpacing": true,
  "arrowParens": "always",
  "endOfLine": "lf"
}

// Why these settings for MCP servers:
// trailingComma: "all" — cleaner diffs when adding zod fields to tool input schemas:
//
// Before (no trailing comma):
// const schema = z.object({
//   query: z.string(),
//   limit: z.number()   ← adding a field here changes THIS line too
// });
//
// After (with trailing comma):
// const schema = z.object({
//   query: z.string(),
//   limit: z.number(),  ← only the new line appears in the diff
//   offset: z.number(), ← new line
// });
//
// printWidth: 100 — MCP tool handler code has deep nesting (server.tool → async handler →
// try/catch → return content block) — 80 chars triggers excessive wrapping

Align your .editorconfig with Prettier's settings so editor auto-format and Prettier agree on indentation and line endings. Mismatched .editorconfig and Prettier settings cause files to be re-formatted on every save in some editors, generating noise in git diffs.

# .editorconfig — must match .prettierrc settings
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.{ts,js,json}]
indent_size = 2

[Makefile]
indent_style = tab  # Makefiles require tabs — Prettier ignores Makefiles

Prettier ignore file for MCP server projects

A .prettierignore file follows the same syntax as .gitignore. Without it, Prettier formats every file it can parse — including node_modules/ if you run prettier --write . from the project root (Prettier ignores node_modules/ by default since v3, but it's still good practice to be explicit about what you exclude).

# .prettierignore — files Prettier should not format

# Build output
dist/
build/
*.d.ts
*.js.map

# Dependencies
node_modules/

# Generated JSON fixtures used in tests (often not valid JS/TS, just data)
tests/fixtures/**/*.json
tests/__snapshots__/

# Auto-generated MCP tool schemas (machine-generated, not hand-written)
src/generated/

# Large data files
**/*.csv
**/*.parquet

# Lock files — Prettier can format these, but it causes merge conflicts
# with npm/yarn/pnpm tools that re-generate them in their own format
package-lock.json
yarn.lock
pnpm-lock.yaml

MCP server projects that generate tool input schemas from OpenAPI or similar specs often have large auto-generated TypeScript files in a src/generated/ directory. Always add these to .prettierignore — formatting them adds noise to commits and resets any non-standard formatting the generator uses intentionally.

Pre-commit formatting with lint-staged

Running prettier --write . on the entire project in a pre-commit hook is slow for large MCP codebases. lint-staged runs formatters only on the files staged for the current commit, reducing pre-commit time from seconds to milliseconds on most changes.

# Install lint-staged (husky integration covered in the Husky guide)
npm install -D lint-staged
// package.json — lint-staged config (can also be in .lintstagedrc.json)
{
  "lint-staged": {
    // Format TypeScript and JavaScript files with Prettier on commit
    "*.{ts,js,mjs,cjs}": [
      "prettier --write",
      "eslint --fix --max-warnings=0"
    ],

    // Format JSON, YAML, Markdown — but NOT lock files
    "*.{json,yaml,yml,md}": [
      "prettier --write"
    ],

    // Do NOT include *.d.ts or dist/ — they're in .prettierignore
    // lint-staged respects .prettierignore automatically when you call prettier --write
  }
}

// Note on order: prettier --write runs FIRST, then eslint --fix
// This matters because prettier may change formatting that would then trigger
// ESLint's --fix to touch the file again. Prettier first = stable output.

The --max-warnings=0 flag on eslint --fix causes lint-staged to fail the commit if ESLint reports any warnings after auto-fixing. This is stricter than the default (which only fails on errors) but prevents warning accumulation over time. If your team prefers a more lenient policy, remove the flag or set it to a higher number.