How to Build an MCP Server in NestJS (A Practical Guide for Backend Devs)
A practical guide for NestJS developers on exposing existing services as MCP tools — why tool descriptions, error messages, and scope matter more than the framework, and where most tool-calling setups break.
How to Build an MCP Server in NestJS
Every AI assistant is now shipping with tool-calling support, and the Model Context Protocol (MCP) has become the closest thing to a standard for it. If you're a backend developer whose stack is already NestJS, the natural next question isn't "should I learn a new framework for this" — it's "can I expose what I already built as an MCP server without rewriting it."
You can. Most of the friction people hit isn't about NestJS at all — it's about treating MCP like a REST endpoint with extra steps, which it isn't.
What MCP actually asks of your backend
MCP is not just "an API an AI can call." A REST endpoint assumes a human-shaped client: a fixed URL, a known payload shape, a developer reading the docs once and hardcoding the integration. An MCP tool assumes the opposite — the caller is a model deciding, at runtime, whether your tool is relevant, what to put in each field, and how to interpret what comes back.
That difference shows up in three places:
Tool descriptions are load-bearing. In a normal API, your description field is documentation for humans who'll skim it once. In MCP, the description is the only signal the model has when deciding whether to call your tool at all — and again when deciding how to fill in the arguments. A vague description doesn't just look unprofessional, it silently reduces how often and how correctly your tool gets used. Every property needs a description, not just the tool itself.
Errors need to be recoverable by the model, not just legible to a human. If a required field is missing, throwing a generic 400 gives the model nothing to act on. The error message is effectively a second chance at getting the model to try again correctly, so it needs to say what was wrong and what a valid value looks like — in plain language, not a stack trace.
State can't be assumed. A human client remembers what it did in the last request. A model calling your tools across a conversation may not maintain that continuity the way you expect, especially across sessions. Anything your tool needs to know about prior calls has to be explicit — passed in, or fetched from your own storage — not implied.
Where NestJS actually helps here
None of the above requires abandoning NestJS's structure — if anything, its module system maps cleanly onto how MCP servers are organized in practice:
- A dedicated MCP module, separate from your existing REST/GraphQL controllers, is worth the separation even if it wraps the same underlying services. Tool definitions have a different shape and different validation rules than your DTOs, and conflating the two makes both harder to reason about.
- Guards for authentication work the same way they always have, but the failure mode is different — an unauthenticated MCP call should return a message the model can surface to the end user, not a silent 401 the model has no context for.
- Existing services stay untouched. The MCP layer should be a thin translation layer over services you already trust, not a place where business logic gets duplicated. If your notification service, your Prisma repositories, or your BullMQ producers already exist, the MCP tool calls into them — it doesn't reimplement them.
- Interceptors are useful for shaping responses back into whatever structure the MCP transport expects, separate from what your REST controllers return to browser clients.
The mistake that causes most broken tool calls
The single most common failure isn't a framework issue — it's tool scope. Teams new to MCP tend to expose one enormous tool ("manageUsers") with a dozen optional parameters instead of several small, sharply defined tools ("getUserById", "deactivateUser", "listUsersByRole"). Models are far more reliable at picking the right narrow tool than at correctly filling in a sprawling one. If you're seeing inconsistent or wrong tool calls in testing, check tool granularity before you touch your validation logic.
Auth is the part people underestimate
If your MCP server is only ever going to be called by you, locally, a simple API key is fine. The moment it's exposed to any external client — a hosted assistant, a teammate's setup, anything you don't fully control — you need the same posture you'd bring to a public API: scoped tokens, rate limiting, and the assumption that a request claiming to be "the model acting on behalf of a user" still needs to prove which user. Skipping this because "it's just for AI" is how internal tools quietly become the least-audited part of a system.
Where this actually pays off
The realistic win isn't a flashy demo — it's turning operational tooling you already have into something a non-technical teammate or a support workflow can trigger through a conversation instead of a dashboard. Status checks, queue inspection, report generation, anything currently gated behind "ask the engineer to run it" is a strong first candidate, because the tool surface is naturally small and the value of natural-language access is immediate.
Start with one well-scoped tool, get the description and error handling right, and expand from there. The protocol rewards precision far more than coverage.