Outline
export function McpRoute(name: string): MethodDecorator;
export namespace McpRoute {
export function Params(): ParameterDecorator;
}
export class McpAdaptor {
public static upgrade(
app: INestApplication,
options?: McpAdaptor.IOptions,
): Promise<void>;
}@McpRoute() exposes a NestJS controller method as an MCP (Model Context
Protocol) tool.
It is designed for the common “tool server” case: an LLM client connects to
your NestJS application through MCP Streamable HTTP, lists the decorated tools,
and calls them by tool name. @nestia/core analyzes the params and return types
at compile time, then emits MCP-compatible JSON schemas and request validators.
McpAdaptor currently supports stateless Streamable HTTP tool endpoints. It
does not keep Mcp-Session-Id state, and it does not implement MCP resources,
prompts, sampling, or elicitation.
Install @modelcontextprotocol/sdk in the server application before calling
McpAdaptor.upgrade(). @nestia/core loads it only when MCP is enabled.
How to use
undefined
import core from "@nestia/core";
import { Controller } from "@nestjs/common";
export interface ICalcInput {
a: number;
b: number;
}
export interface ICalcResult {
result: number;
}
@Controller()
export class CalculatorController {
@core.McpRoute("add")
public async add(
@core.McpRoute.Params() params: ICalcInput,
): Promise<ICalcResult> {
return { result: params.a + params.b };
}
@core.McpRoute("notify")
public async notify(
@core.McpRoute.Params() params: { message: string },
): Promise<void> {
void params;
}
}At first, decorate a controller method with @McpRoute(). The name becomes
the MCP tool name, so it must be unique in the application.
Then call McpAdaptor.upgrade() before app.listen(). Without the adaptor,
decorated methods are only metadata and no MCP HTTP endpoint will be mounted.
Installation
npm install @modelcontextprotocol/sdk@nestia/core does not force every user to install the MCP SDK. The package is
loaded lazily when McpAdaptor.upgrade() is called. Therefore, applications
using @McpRoute() must install @modelcontextprotocol/sdk themselves.
When generating a distributable SDK package with nestia.config.ts
distribute, @nestia/sdk adds @modelcontextprotocol/sdk to the generated
package dependencies only if MCP routes exist.
Parameters
export namespace McpRoute {
export function Params(): ParameterDecorator;
}Every MCP tool must have exactly one parameter, and it must be decorated with
@McpRoute.Params().
The parameter type must be a single object type without dynamic keys. Index
signatures and Record<string, T> are rejected at compile time because MCP
tool arguments must have statically known JSON schema properties.
@core.McpRoute("get_weather")
public async getWeather(
@core.McpRoute.Params() params: {
location: string;
unit: "celsius" | "fahrenheit";
},
): Promise<{ temperature: number }> {
return { temperature: 24 };
}@core.McpRoute("bad")
public async bad(
@core.McpRoute.Params() params: Record<string, number>,
): Promise<{ ok: boolean }> {
return { ok: true };
}Return Type
MCP tools may return either:
void- a single object type without dynamic keys
void | object unions are rejected because the generated client needs one
stable output contract. Dynamic-key object returns are also rejected for the
same schema reason as params.
For object returns, Nestia serializes the result as JSON text content and the generated SDK wrapper parses it back into the declared output type.
For void returns, Nestia returns an empty MCP content array and the generated
SDK wrapper returns Promise<void>.
Generated SDK
Related Document: Software Development Kit
When you run npx nestia sdk, MCP routes are emitted under
api.functional.mcp.
import type { Client as McpClient } from "@modelcontextprotocol/sdk/client/index.js";
import type { CallToolResult as McpCallToolResult } from "@modelcontextprotocol/sdk/types.js";
export async function add(
client: McpClient,
args: add.Input,
): Promise<add.Output> {
const raw = await client.callTool({
name: add.METADATA.tool,
arguments: args as unknown as Record<string, unknown>,
});
if ("toolResult" in raw)
throw new Error("Legacy MCP compatibility result is not supported.");
const result: McpCallToolResult = raw as McpCallToolResult;
if (result.isError === true) throw new Error("MCP tool returned isError.");
const first = result.content[0];
if (first === undefined || first.type !== "text")
throw new Error("MCP tool returned no text content.");
return JSON.parse(first.text) as add.Output;
}The generated function receives an already connected MCP Client. Nestia does
not create the MCP transport for you, because transport lifecycle, auth headers,
and client identity belong to the caller.
MCP SDK imports are aliased (McpClient, McpCallToolResult) so that your DTO
names may still be Client or CallToolResult.
Restrictions
Stateless Endpoint
McpAdaptor is stateless. It creates a fresh MCP server and Streamable HTTP
transport for each request. This is the MCP SDK’s recommended pattern for
stateless Streamable HTTP servers.
Do not pass sessioned. Stateful Mcp-Session-Id support requires a session
store, transport reuse, cleanup policy, and deployment rules such as sticky
sessions. That is intentionally outside the current McpAdaptor scope.
Unique Tool Names
MCP tools are dispatched by name. Therefore, two @McpRoute() methods cannot
share the same tool name.
@core.McpRoute("duplicated")
public async first(
@core.McpRoute.Params() params: { value: string },
): Promise<{ ok: boolean }> {
return { ok: true };
}
@core.McpRoute("duplicated")
public async second(
@core.McpRoute.Params() params: { value: string },
): Promise<{ ok: boolean }> {
return { ok: true };
}Duplicate tool names are rejected during SDK generation and again during
McpAdaptor.upgrade() bootstrap.