Skip to main content
Glama
jaenster

nestjs-mcp-controller

by jaenster

nestjs-mcp-controller

Build Model Context Protocol (MCP) servers inside a NestJS application using ordinary controllers. Declare tools, resources and prompts as decorated methods — the library discovers them, wires them to an MCP server over Streamable HTTP, and gives you NestJS-native middleware, guards and OAuth on top.

It is designed to be embedded in an existing platform: the platform keeps its own authentication and simply tells the library how to validate a bearer token. Per-tool scopes and roles mean a logged-in user only ever sees — and can only call — the tools they are allowed to.

@Controller()
export class MathController {
  @Tool('add_numbers', { inputSchema: { a: z.number(), b: z.number() } })
  add({ a, b }: { a: number; b: number }) {
    return `${a + b}`;
  }
}

Features

  • @Tool(), @Resource(), @Prompt() on any @Controller() / @Injectable() — auto-discovered across the app.

  • Built on the official @modelcontextprotocol/sdk (spec-compliant, tracks upstream).

  • Zod schemas for input/output — validated before your method runs.

  • Streamable HTTP transport with stateful sessions (or stateless mode).

  • Middleware pipeline (McpMiddleware) — global, per-controller or per-method; the MCP analogue of interceptors.

  • OAuth, layered:

    • Resource server (default): plug in a McpTokenVerifier; the library validates bearer tokens and serves RFC 9728 protected-resource metadata.

    • Authorization server (optional): mount the SDK's OAuth endpoints with McpAuthServerModule for standalone deployments.

  • Per-tool authorization with @RequireScopes() / @RequireRoles() — unauthorized primitives are hidden from list and rejected on call.

Related MCP server: easy-mcp

Install

pnpm add nestjs-mcp-controller @modelcontextprotocol/sdk zod
# peer deps (you already have these in a Nest app)
pnpm add @nestjs/common @nestjs/core @nestjs/platform-express reflect-metadata rxjs

Quick start

import { Module, Controller } from '@nestjs/common';
import { McpModule, Tool, z } from 'nestjs-mcp-controller';

@Controller()
export class MathController {
  @Tool('add_numbers', {
    description: 'Add two numbers',
    inputSchema: { a: z.number(), b: z.number() },
  })
  add({ a, b }: { a: number; b: number }) {
    return `${a + b}`; // string | object | a full CallToolResult — all accepted
  }
}

@Module({
  imports: [McpModule.forRoot({ server: { name: 'my-mcp', version: '1.0.0' } })],
  controllers: [MathController],
})
export class AppModule {}

The MCP endpoint is served at POST/GET/DELETE /mcp (configurable via path). Point any MCP client (Claude Desktop, the MCP Inspector, the SDK client) at it.

Handler return values

A @Tool method may return:

  • a string → wrapped as a single text block,

  • any object → JSON-stringified into a text block and attached as structuredContent,

  • a full CallToolResult ({ content: [...] }) → passed through untouched.

Every handler also receives an McpContext as its last argument:

@Tool('whoami')
whoami(_args: unknown, ctx: McpContext) {
  return { clientId: ctx.authInfo?.clientId, scopes: ctx.scopes, session: ctx.sessionId };
}

ctx carries authInfo, scopes, sessionId and the raw SDK extra (abort signal, sendNotification for progress, …).

Resources & prompts

@Resource({ uri: 'config://app', mimeType: 'application/json' })
appConfig(uri: URL) {
  return { contents: [{ uri: uri.href, text: JSON.stringify(this.config) }] };
}

// templated URIs work too: receive (uri, variables, ctx)
@Resource({ uri: 'users://{id}' })
user(uri: URL, { id }: { id: string }) {
  return { contents: [{ uri: uri.href, text: JSON.stringify(this.find(id)) }] };
}

@Prompt({ name: 'review_code', argsSchema: { code: z.string() } })
review({ code }: { code: string }) {
  return { messages: [{ role: 'user', content: { type: 'text', text: `Review:\n${code}` } }] };
}

Middleware

Around-style middleware — log, time, rate-limit, mutate args or shape results. Resolved from DI, so it can inject services.

@Injectable()
export class LoggingMiddleware implements McpMiddleware {
  async use(ctx: McpContext, next: McpNext) {
    const start = Date.now();
    const result = await next();
    console.log(`${ctx.kind} ${ctx.name} in ${Date.now() - start}ms`);
    return result;
  }
}

Apply it globally, per-controller or per-method:

// global
McpModule.forRoot({ server, middleware: [LoggingMiddleware] });

// per controller / method (outermost = global → class → method)
@UseMcpMiddleware(LoggingMiddleware)
@Controller()
class BillingController {
  @Tool('refund') @UseMcpMiddleware(RateLimitMiddleware) refund() { /* ... */ }
}

Register middleware that needs singleton services as providers in a module visible to McpModule; otherwise it is instantiated transiently per call.

Authentication & authorization

Implement a McpTokenVerifier that turns a bearer token into McpAuthInfo. The platform keeps its own OAuth/login; this is just the validation seam.

@Injectable()
export class JwtVerifier implements McpTokenVerifier {
  async verify(token: string, req: Request): Promise<McpAuthInfo | null> {
    const claims = await verifyJwt(token); // e.g. with `jose`
    if (!claims) return null;
    return {
      token,
      clientId: claims.sub,
      scopes: claims.scope?.split(' ') ?? [],
      expiresAt: claims.exp,
      extra: { roles: claims.roles, tenantId: claims.tenant }, // your identity
    };
  }
}

McpModule.forRoot({
  server: { name: 'my-mcp', version: '1.0.0' },
  auth: {
    enabled: true,
    requireAuth: true,                 // 401 + RFC 9728 challenge when invalid/absent
    verifier: JwtVerifier,             // instance or class (DI-resolved)
    resourceUrl: 'https://api.example.com/mcp',
    authorizationServers: ['https://auth.example.com'],
  },
});

With resourceUrl set, the library serves GET /.well-known/oauth-protected-resource so clients can discover the authorization server.

Set requireAuth: false to allow anonymous access while still enforcing per-tool scope/role checks (anonymous callers simply don't pass them).

Per-tool authorization

@RequireScopes() / @RequireRoles() apply to a method or an entire controller (class-level requirements are inherited and merged). A caller missing the requirement won't see the tool in list and can't call it.

@Controller()
@RequireScopes('billing:read')           // applies to every tool in the class
export class BillingController {
  @Tool('list_invoices') list() { /* needs billing:read */ }

  @Tool('refund_invoice', { inputSchema: { id: z.string() } })
  @RequireScopes('billing:write')        // additionally needs billing:write
  @RequireRoles('admin')                 // and the admin role
  refund({ id }: { id: string }) { /* ... */ }
}

Roles are read from authInfo.extra.roles by default; override with auth.rolesResolver.

Because a session is bound to one identity, tools are filtered when the session is created and re-checked on every call.

Authorization server (optional, standalone)

When you are not embedding behind an existing OAuth server, mount the SDK's authorization endpoints:

@Module({
  imports: [
    McpAuthServerModule.forRoot({ provider, issuerUrl: new URL('https://auth.example.com') }),
    McpModule.forRoot({ /* ... */ }),
  ],
})
export class AppModule {}

Configuration

McpModule.forRoot(options) / McpModule.forRootAsync(asyncOptions):

Option

Default

Description

server

{ name, version, instructions? } reported on initialize

path

mcp

HTTP route for the transport (forRoot only)

stateful

true

Per-client sessions with Mcp-Session-Id; false = stateless

middleware

[]

Global middleware classes

auth

disabled

{ enabled, requireAuth, verifier, resourceUrl, authorizationServers, rolesResolver }

allowedHosts

Host allowlist for DNS-rebinding protection

isGlobal

false

Register the module globally

forRootAsync supports useFactory / useClass / useExisting (implementing McpOptionsFactory).

Example

A runnable example lives in example/ — billing tools with scopes, roles, a logging middleware and a verifier. Run it with npx ts-node example/main.ts and connect with header Authorization: Bearer root:billing:read,billing:write|admin.

Development

pnpm install
pnpm build      # tsc → dist/
pnpm test       # jest e2e: real MCP client over HTTP
pnpm lint

License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    A NestJS module for building Model Context Protocol (MCP) servers using decorators to expose services as tools, resources, and prompts. It features auto-discovery, a built-in playground UI, and support for multiple transports including SSE and Stdio.
    11 npm
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Simplifies creating MCP servers in TypeScript with an Express-like API and experimental decorators, enabling quick definition of tools, resources, and prompts.
    25 npm
    196
    MIT
  • F
    license
    B
    quality
    D
    maintenance
    Experimental MCP server built with NestJS and LangGraph, enabling tool execution and agent workflows via stdio transport. Supports OpenAI and Ollama models for flexible agent orchestration.
    4
    -
  • F
    license
    Not graded
    quality
    C
    maintenance
    A MCP server framework with zero-config auto-discovery, type-safe decorators, and HTTP transport, enabling easy creation and deployment of tools, prompts, and resources to LeanMCP Cloud.
    -