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-prettier — eslint-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.