When an AI-based application needs to interact with real business systems, the Model Context Protocol (MCP) has become the de facto standard. However, bringing an MCP server to a production environment with NestJS involves much more than following the basic SDK documentation. It requires an architecture that respects the principles of dependency injection, per-tenant isolation, and perimeter security. At Q2BSTUDIO, as a software and technology development company, we've implemented this pattern across multiple clients, combining enterprise AI with AI agent capabilities without sacrificing backend robustness.
The main challenge arises when the team decides to connect a language model to existing services. A typical NestJS REST API knows how to handle authentication, business logic, and multi-tenant. But an LLM can't invoke those endpoints directly unless there's a tool layer that the model understands. The CCM defines precisely that interface: a set of tools with schemas and descriptions that the model interprets to decide what to call and with what arguments. The initial temptation is to set up a fast MCP server with Express in a few lines, but that means duplicating the database, session, and logging access logic that NestJS already has. The right approach is to integrate the MCP server within the same dependency injection container, so that each tool can use the same services that the rest of the application uses.
That's precisely what we've done in recent projects. Instead of writing a single file of 900 lines with closures, you define a class per tool domain, decorated with @Injectable() and which receives the necessary services in its constructor (for example, an ordering service). Each class exposes a register() method that receives an instance of McpServer and a execution context. That context contains the tenant and environment information (sandbox or production), which has been previously extracted from the API key. Registering the tool within that method allows the SDK to infer types automatically from the Zod schema, avoiding complex generics. A central factory then collects all the tools using a multi-provider token and builds a new server for each request. This way, each request to the MCP server has its own tenant context, and the controller is only responsible for connecting the Streamable HTTP transport in a stateless manner.
The choice of stateless mode is not accidental. For a server that responds to AI agents in production, the simplest thing to do is not to maintain sessions between requests. Each call to the /mcp endpoint creates a new server, processes the tool, and returns the response. This simplifies horizontal scaling and makes it easy to integrate with load balancers. The NestJS driver injects @Res() to prevent response interceptors from interfering with streaming, and makes sure to clean up the transport when closing the connection. That controller should remain as simple as possible, delegating all business logic to the registered tools.
One of the most important lessons we've learned from putting this into production is that the quality of the tooltips determines the behavior of the model. It is not enough to put types and names. The expected behavior and restrictions must be described. For example, instead of a 'limit: number' parameter, it is better to write 'Maximum number of orders to return, sorted from newest to oldest'. It is also convenient to narrow down the possible values with enumerations and default values, because a model can invent states or IDs that do not exist. We've seen how splitting a generic search tool into several small ones (list orders, get order by ID, search by customer) drastically reduces invocation errors. And when something goes wrong, it's best to return a structured error with isError: true and a clear message indicating which alternative to use, rather than throwing an exception that the model doesn't know how to interpret.
The multi-tenant scope is another critical point. It's very tempting to add a 'tenantId' parameter to each tool for the model to provide, but that's a huge security risk: a model can hallucinate a tenant that doesn't belong to it. The correct solution is for the tenant's identity to be resolved from the credential (the API key) before the request is processed by the MCP server, and for that value to be injected into the context of all tools by shutting down the factory. This way, the model never sees the tenantId; it simply operates within the boundaries of the authenticated tenant. The same goes for the environment: a sandbox key allows access to test tools, such as seeding test data, while a production key hides them. This is implemented with a simple if in the tool's register() method.
The testing of this architecture also has its particularities. For unit tests, the tool can be instantiated directly with the NestJS testing module and mocked services, without the need for MCP infrastructure. But for integration testing, the SDK provides a pair of in-memory transports that allow you to simulate the entire stream without HTTP. A server is created with the factory, connected to an in-memory transport, an MCP client is created that calls the tool, and both the arguments received by the mock service and the formatted response are verified. This detects schema errors that unit tests don't capture, such as the Zod schema not matching what the tool expects.
At Q2BSTUDIO, we combine these techniques with other capabilities we offer as part of our bespoke software services. For example, when a customer needs to integrate AI agents with their legacy systems, we not only deploy the MCP server, but also design the business logic with bespoke applications that communicate securely. In addition, we secure the entire flow with cybersecurity measures such as API key authentication and restriction of tools by environment. In many cases, these agents are deployed on top of AWS and Azure cloud services, leveraging balancers and autoscaling. We've even integrated these MCP servers with power bi dashboards so that end users can see which tools have been executed, as part of broader business intelligence services.
From a business perspective, the decision to adopt MCPs with NestJS reduces friction between backend and AI teams. NestJS developers can continue to write services with their usual flow, and data scientists or prompt engineers only need to know the tool interface. This accelerates the creation of AI agents that actually solve business problems, such as automated customer service or order management. And when you add analytics tools, such as database queries or external API invocations, the same architecture scales without rewriting.
The result, after several deployments in production, is a set of approximately 300 lines of additional code on top of the existing NestJS application. Most of it corresponds to the definitions of the tools, which would have to be written anyway. The protocol itself is the easy part; What makes the difference is how it integrates with the rest of the system. Keeping transport in a single controller, resolving the tenant before the SDK comes into play, and treating descriptions as the true API surface are the keys to making an MCP server in production not a problem, but an AI enabler.


