NitroStack Logo
/sdk
/typescript
/server

Server Concepts

Overview

NitroStack provides a NestJS-inspired architecture built around decorators, modules, and dependency injection. This guide covers the fundamental concepts required to build production-ready MCP servers.

Table of Contents

Application Bootstrap

Prerequisites

NitroStack requires TypeScript decorator metadata to function. Ensure reflect-metadata is installed:

Bash
npm install @nitrostack/core reflect-metadata zod

In your tsconfig.json:

JSON
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022"
  }
}

Application Entry Point

Important: import 'reflect-metadata' must be the very first import in your entry file (src/index.ts). Without it, TypeScript decorators cannot persist runtime metadata.

Typescript
// src/index.ts
import 'reflect-metadata'; // Must be first
import { McpApplicationFactory } from '@nitrostack/core';
import { AppModule } from './app.module.js';

async function bootstrap() {
  const server = await McpApplicationFactory.create(AppModule);
  
  // Starts with defaults from @McpApp, or pass programmatic overrides:
  await server.start({
    transport: 'dual', // 'stdio' | 'http' | 'dual'
    port: 8080,        // Custom HTTP port
    host: '0.0.0.0',   // Custom host binding
    endpoint: '/mcp',  // MCP base path (default: '/mcp')
    enableCors: true,  // CORS headers for browser clients
  });
}

bootstrap();

ServerStartOptions Reference

When calling server.start(options), the following options can override @McpApp decorator defaults:

PropertyTypeDefaultDescription
transport'stdio' | 'http' | 'dual'Value from @McpAppTransport listener mode.
portnumber3000Port for HTTP listeners.
hoststring'0.0.0.0'Host interface binding.
endpointstring'/mcp'Base path for streamable MCP routes.
enableCorsbooleantrueEnables Cross-Origin Resource Sharing for browser web clients.

@McpApp Decorator

The @McpApp decorator configures your root application module, transport modes, and server metadata:

Typescript
// src/app.module.ts
import {
  McpApp,
  Module,
  ConfigModule,
  BM25SearchTransform,
} from '@nitrostack/core';
import { ProductsModule } from './modules/products/products.module.js';

@McpApp({
  module: AppModule,
  server: {
    name: 'bookshelf-server',
    version: '1.0.0',
    protocolVersion: 'auto', // 'auto' (default: 2026-07-28 + legacy fallback) | '2026-07-28' | 'legacy'
  },
  logging: {
    level: 'info', // 'debug' | 'info' | 'warn' | 'error'
  },
  transport: {
    type: 'dual', // 'stdio' | 'http' | 'dual'
    http: {
      port: 3000,
      host: '0.0.0.0',
      basePath: '/mcp',
    },
  },
  shutdownHooks: true, // Graceful SIGINT/SIGTERM lifecycle execution
  transforms: [
    // Progressive tool discovery pipeline to minimize LLM token consumption
    new BM25SearchTransform({
      defaultLimit: 5,
      alwaysVisible: ['products_search_catalog'],
    }),
  ],
})
@Module({
  imports: [
    ConfigModule.forRoot(),
    ProductsModule,
  ],
})
export class AppModule {}

Protocol Version & MCP 2026-07-28 Dual-Spec

NitroStack provides dual-engine protocol support:

  • auto (Default): Dual-adapter that serves the modern MCP 2026-07-28 stateless protocol while maintaining backwards compatibility for 2025-era sessionful clients.
  • 2026-07-28: Enforces pure modern stateless HTTP over /mcp (server/discover, per-request _meta envelope, Multi Round-Trip Requests via inputRequired).
  • legacy (or 2025-06-18): Preserves sessionful Streamable HTTP with Mcp-Session-Id.

[!TIP] The environment variable NITRO_MCP_PROTOCOL_VERSION takes precedence over @McpApp({ server: { protocolVersion } }), allowing DevOps operators to switch deployed protocol modes without modifying code.

The factory performs the following initialization sequence:

  1. Reads @McpApp metadata from the root module
  2. Initializes the logging subsystem
  3. Configures the dependency injection container
  4. Registers all imported modules recursively
  5. Builds and registers tools, resources, and prompts
  6. Starts the MCP server transport (stdio, HTTP SSE, or dual)

Server Storage & Data Spillover Configuration

To prevent oversized tool outputs from overflowing model context windows and exhausting Node.js heap memory, servers can configure default spillover storage:

Typescript
import { createServer } from '@nitrostack/core';

// Production filesystem configuration:
const server = createServer({
  name: 'enterprise-server',
  version: '1.0.0',
  spillover: {
    driver: 'filesystem',
    storageDir: './.spillover_cache',
    maxSizeBytes: 200 * 1024 * 1024, // 200MB maximum disk quota
  },
});

// Development in-memory configuration:
const devServer = createServer({
  name: 'dev-server',
  version: '1.0.0',
  spillover: {
    driver: 'memory',
    maxSizeBytes: 20 * 1024 * 1024,  // 20MB in-memory buffer
  },
});

When configured, NitroStack automatically exposes the built-in resource://data-spillover/{id} endpoint, allowing AI models to inspect spilled datasets on demand via standard MCP resources/read.

Modules

Module Architecture

Modules organize your application into cohesive, loosely-coupled units. Each module encapsulates related functionality and declares its dependencies explicitly.

Typescript
import { Module } from '@nitrostack/core';

@Module({
  name: 'products',
  description: 'Product catalog management',
  controllers: [ProductsTools, ProductsResources, ProductsPrompts],
  providers: [ProductService, DatabaseService],
  imports: [HttpModule],
  exports: [ProductService]
})
export class ProductsModule {}

Module Properties Reference

PropertyTypeDescriptionRequired
namestringUnique module identifierYes
descriptionstringHuman-readable descriptionNo
controllersClass[]Tool, resource, and prompt classesNo
providersClass[]Services registered with the DI containerNo
importsModule[]Modules whose exports are availableNo
exportsClass[]Providers available to importing modulesNo
globalbooleanIf true, providers are available globallyNo

Controllers

Controllers contain your MCP primitives (tools, resources, and prompts):

Typescript
// products.tools.ts
import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core';

export class ProductsTools {
  constructor(private productService: ProductService) {}

  @Tool({
    name: 'get_product',
    description: 'Retrieve product details by ID',
    inputSchema: z.object({
      product_id: z.string().describe('Product ID'),
    }),
  })
  async getProduct(input: { product_id: string }, ctx: ExecutionContext) {
    return this.productService.findById(input.product_id);
  }
}

// products.resources.ts
import { ResourceDecorator as Resource, ExecutionContext } from '@nitrostack/core';

export class ProductsResources {
  constructor(private productService: ProductService) {}

  @Resource({
    uri: 'product://{id}',
    name: 'Product Details',
    mimeType: 'application/json'
  })
  async getProductResource(uri: string, ctx: ExecutionContext) {
    const id = uri.split('://')[1];
    const product = await this.productService.findById(id);
    return {
      contents: [{
        uri,
        mimeType: 'application/json',
        text: JSON.stringify(product, null, 2)
      }]
    };
  }
}

// products.prompts.ts
import { PromptDecorator as Prompt, ExecutionContext } from '@nitrostack/core';

export class ProductsPrompts {
  @Prompt({
    name: 'review_product',
    description: 'Generate a product review prompt'
  })
  async getReviewPrompt(args: { product_id: string }, ctx: ExecutionContext) {
    return [
      {
        role: 'user' as const,
        content: { type: 'text' as const, text: `Review product ${args.product_id}` }
      }
    ];
  }
}

Providers (Services)

Providers encapsulate business logic and can be injected into controllers or other providers:

Typescript
import { Injectable } from '@nitrostack/core';

@Injectable()
export class ProductService {
  constructor(private db: DatabaseService) {}

  async findById(id: string): Promise<Product | null> {
    return this.db.query('SELECT * FROM products WHERE id = $1', [id]);
  }

  async search(query: string, options?: SearchOptions): Promise<Product[]> {
    return this.db.query(
      'SELECT * FROM products WHERE name ILIKE $1 LIMIT $2 OFFSET $3',
      [`%${query}%`, options?.limit ?? 20, options?.offset ?? 0]
    );
  }

  async create(data: CreateProductDto): Promise<Product> {
    const result = await this.db.query(
      'INSERT INTO products (name, price, description) VALUES ($1, $2, $3) RETURNING *',
      [data.name, data.price, data.description]
    );
    return result[0];
  }
}

Module Imports and Exports

Imports allow a module to use providers exported by other modules:

Typescript
@Module({
  name: 'orders',
  imports: [ProductsModule, PaymentsModule],  // Use exports from these modules
  controllers: [OrdersTools],
  providers: [OrderService]
})
export class OrdersModule {}

Exports make providers available to importing modules:

Typescript
@Module({
  name: 'products',
  providers: [ProductService, InternalHelper],
  exports: [ProductService]  // Only ProductService is available to importers
})
export class ProductsModule {}

Dependency Injection

@Injectable Decorator

Mark classes for dependency injection:

Typescript
import { Injectable } from '@nitrostack/core';

@Injectable()
export class EmailService {
  constructor(private configService: ConfigService) {}

  async send(to: string, subject: string, body: string): Promise<void> {
    const smtpHost = this.configService.get('SMTP_HOST');
    // Implementation
  }
}

Constructor Injection

Dependencies are resolved and injected automatically via constructor parameters:

Typescript
export class UserTools {
  constructor(
    private userService: UserService,
    private emailService: EmailService,
    private auditService: AuditService
  ) {}

  @Tool({ name: 'create_user', description: 'Create a new user account' })
  async createUser(input: CreateUserInput, ctx: ExecutionContext) {
    const user = await this.userService.create(input);
    await this.emailService.send(user.email, 'Welcome', 'Account created');
    await this.auditService.log('user.created', { userId: user.id });
    return user;
  }
}

DI Container Behavior

The dependency injection container:

  1. Resolves dependencies: Analyzes constructor parameters and resolves types
  2. Creates instances: Instantiates classes with resolved dependencies
  3. Manages lifecycle: Services are singleton by default (one instance per application)
  4. Handles circular dependencies: Detects and reports circular dependency errors

Provider Scopes

By default, all providers are singleton scoped:

Typescript
@Injectable()
export class DatabaseService {
  // Single instance shared across the entire application
  private pool: Pool;

  constructor() {
    this.pool = new Pool(/* config */);
  }
}

Configuration

ConfigModule

The ConfigModule provides centralized configuration management:

Typescript
import { Module, ConfigModule } from '@nitrostack/core';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: '.env',
      isGlobal: true,
      validate: (config) => {
        const required = ['DATABASE_URL', 'JWT_SECRET'];
        for (const key of required) {
          if (!config[key]) {
            throw new Error(`Missing required environment variable: ${key}`);
          }
        }
        return config;
      }
    })
  ]
})
export class AppModule {}

ConfigService

Access configuration values in your services:

Typescript
import { Injectable, ConfigService } from '@nitrostack/core';

@Injectable()
export class DatabaseService {
  private connectionString: string;

  constructor(private config: ConfigService) {
    this.connectionString = this.config.get('DATABASE_URL');
  }

  getPort(): number {
    return this.config.get('PORT', 3000);  // With default value
  }

  isProduction(): boolean {
    return this.config.get('NODE_ENV') === 'production';
  }
}

Execution Context

Every tool, resource, and prompt handler receives an ExecutionContext object providing access to authentication, logging, request metadata, and session state:

Typescript
interface ExecutionContext {
  /** Structured logger instance */
  logger: Logger;

  /** Name of the current tool (if applicable) */
  toolName?: string;

  /** Request identifier for tracing */
  requestId: string;

  /** Active transport connection or session identifier */
  sessionId?: string;

  /** Standard AbortSignal triggered if client cancels invocation */
  abortSignal?: AbortSignal;

  /** Authentication information (populated by guards) */
  auth?: {
    subject?: string;
    token?: string;
    scopes?: string[];
    [key: string]: unknown;
  };

  /** Verified subject from cryptographic identity or token */
  verifiedSubject?: string;

  /** Request metadata storage */
  metadata?: Record<string, unknown>;

  /** MCP Background Task tracking metadata when taskSupport is active */
  task?: {
    taskId: string;
    status: 'working' | 'completed' | 'failed' | 'cancelled';
    totalSteps?: number;
    currentStep?: number;
    [key: string]: unknown;
  };

  /** Client responses to prior inputRequired elicitations (SEP-2322) */
  inputResponses?: Record<string, unknown>;

  /** Opaque round-trip state echoed by client (SEP-2322) */
  requestState?: unknown;

  /** Dynamic tool visibility helpers */
  enableTools?: (toolNames: string[]) => void | Promise<void>;
  disableTools?: (toolNames: string[]) => void | Promise<void>;
}

Usage example:

Typescript
import { ToolDecorator as Tool, UseGuards, ExecutionContext, emitEvent } from '@nitrostack/core';
import { JWTGuard } from './guards/jwt.guard.js';

@Tool({ name: 'create_order', description: 'Create a new order' })
@UseGuards(JWTGuard)
async createOrder(input: CreateOrderInput, ctx: ExecutionContext) {
  const userId = ctx.auth?.subject;
  ctx.logger.info('Creating order', { userId, input });

  const order = await this.orderService.create(input, userId);
  
  // Emit event across event bus
  await emitEvent('order.created', { orderId: order.id, userId });

  return order;
}

Lifecycle

NitroStack provides a predictable, deterministic lifecycle model mirroring NestJS. Providers, controllers, and modules can hook into both startup initialization and graceful shutdown phases.

Lifecycle Sequence

Text
STARTUP PHASE
┌───────────────────────────────────────────────┐
│ 1. McpApplicationFactory.create(AppModule)    │
│    Dependency graph compiled & validated      │
└───────────────────────┬───────────────────────┘
                        │
                        ▼
┌───────────────────────────────────────────────┐
│ 2. onModuleInit()                             │
│    Executed on all providers & controllers    │
└───────────────────────┬───────────────────────┘
                        │
                        ▼
┌───────────────────────────────────────────────┐
│ 3. Server transports start listening          │
│    STDIO / HTTP / SSE ready for connections   │
└───────────────────────┬───────────────────────┘
                        │
                        ▼
┌───────────────────────────────────────────────┐
│ 4. onApplicationBootstrap()                   │
│    Executed once server is fully operational  │
└───────────────────────────────────────────────┘

SHUTDOWN PHASE (SIGINT / SIGTERM / app.close())
┌───────────────────────────────────────────────┐
│ 5. beforeApplicationShutdown(signal)          │
│    Notification before closing transports     │
└───────────────────────┬───────────────────────┘
                        │
                        ▼
┌───────────────────────────────────────────────┐
│ 6. Server transports cease listening          │
│    Existing in-flight requests drain          │
└───────────────────────┬───────────────────────┘
                        │
                        ▼
┌───────────────────────────────────────────────┐
│ 7. onModuleDestroy()                          │
│    Release external resources, DB pools       │
└───────────────────────┬───────────────────────┘
                        │
                        ▼
┌───────────────────────────────────────────────┐
│ 8. onApplicationShutdown(signal)              │
│    Final teardown before Node process exit    │
└───────────────────────────────────────────────┘

Lifecycle Hook Interfaces

NitroStack exports 5 formal lifecycle interfaces from @nitrostack/core:

InterfaceMethod SignatureInvocation TimingTypical Use Cases
OnModuleInitonModuleInit(): Promise<void> | voidAfter DI container resolves all dependencies, before server listens.Initialize database connections, pre-compile schemas, warm internal caches.
OnApplicationBootstraponApplicationBootstrap(): Promise<void> | voidAfter server transports (STDIO/HTTP) are actively listening.Health-check pings, registering server with service registries, startup alerts.
BeforeApplicationShutdownbeforeApplicationShutdown(signal?: string): Promise<void> | voidTriggered immediately when termination signal (SIGINT/SIGTERM) is received, prior to closing connections.Stop accepting new work, flush pending telemetry buffers, notify peers.
OnModuleDestroyonModuleDestroy(): Promise<void> | voidAfter transport listeners are closed.Close database connection pools, terminate child worker processes, release file handles.
OnApplicationShutdownonApplicationShutdown(signal?: string): Promise<void> | voidFinal cleanup step before the application context is completely destroyed.Final logging, flushing stdout/stderr, cleanup temporary filesystem sockets.

Lifecycle Implementation Example

Classes decorated with @Injectable() or @Controller() can implement any combination of these interfaces:

Typescript
import {
  Injectable,
  OnModuleInit,
  OnApplicationBootstrap,
  BeforeApplicationShutdown,
  OnModuleDestroy,
  OnApplicationShutdown,
} from '@nitrostack/core';

@Injectable()
export class DatabaseConnectionPool implements
  OnModuleInit,
  OnApplicationBootstrap,
  BeforeApplicationShutdown,
  OnModuleDestroy,
  OnApplicationShutdown {
  
  private isConnected = false;

  async onModuleInit(): Promise<void> {
    console.log('[DatabaseConnectionPool] 1. Initializing connection pool...');
    // Establish connection to database
    this.isConnected = true;
  }

  async onApplicationBootstrap(): Promise<void> {
    console.log('[DatabaseConnectionPool] 2. Application ready; verifying connection readiness...');
    if (!this.isConnected) {
      throw new Error('Database pool failed to initialize during startup');
    }
  }

  async beforeApplicationShutdown(signal?: string): Promise<void> {
    console.log(`[DatabaseConnectionPool] 3. Shutdown initiated (${signal}); draining in-flight queries...`);
    // Allow pending queries a grace window to finish
  }

  async onModuleDestroy(): Promise<void> {
    console.log('[DatabaseConnectionPool] 4. Closing database pool connections...');
    this.isConnected = false;
  }

  async onApplicationShutdown(signal?: string): Promise<void> {
    console.log(`[DatabaseConnectionPool] 5. Teardown complete. Exiting via ${signal}.`);
  }
}

Safe Execution & Error Handling

NitroStack invokes lifecycle hooks sequentially across all registered instances using triggerLifecycleHook. By default, if any onModuleInit hook throws an error, the bootstrap process aborts immediately, preventing an unready server from listening to requests.

Graceful shutdown hooks are enabled by default via shutdownHooks: true in @McpApp. When enabled, NitroStack automatically intercepts SIGINT and SIGTERM signals from the operating system and coordinates the orderly teardown sequence.

Request Lifecycle

Each incoming request follows this pipeline:

Request Arrives (STDIO/HTTP)
        │
        ▼
  Route to Handler
        │
        ▼
   Middleware (pre)
        │
        ▼
      Guards
        │
        ▼
       Pipes
        │
        ▼
  Handler Execution
        │
        ▼
    Interceptors
        │
        ▼
 Exception Filters
        │
        ▼
  Middleware (post)
        │
        ▼
   Send Response

Module Organization

src/
├── modules/
│   ├── auth/
│   │   ├── auth.module.ts
│   │   ├── auth.tools.ts
│   │   ├── auth.service.ts
│   │   └── guards/
│   │       └── jwt.guard.ts
│   ├── products/
│   │   ├── products.module.ts
│   │   ├── products.tools.ts
│   │   ├── products.resources.ts
│   │   ├── products.prompts.ts
│   │   ├── products.service.ts
│   │   └── dto/
│   │       ├── create-product.dto.ts
│   │       └── update-product.dto.ts
│   └── orders/
│       ├── orders.module.ts
│       ├── orders.tools.ts
│       └── orders.service.ts
├── common/
│   ├── middleware/
│   ├── interceptors/
│   ├── pipes/
│   └── filters/
├── app.module.ts
└── index.ts

Feature Modules

Organize code by business domain:

Typescript
// Feature module encapsulates all product-related functionality
@Module({
  name: 'products',
  controllers: [ProductsTools, ProductsResources],
  providers: [ProductService, ProductRepository],
  exports: [ProductService]
})
export class ProductsModule {}

Shared Modules

Create reusable modules for cross-cutting concerns:

Typescript
@Module({
  name: 'database',
  providers: [DatabaseService, TransactionManager],
  exports: [DatabaseService, TransactionManager],
  global: true  // Available to all modules without explicit import
})
export class DatabaseModule {}

Core Module

Essential application-wide services:

Typescript
@Module({
  name: 'core',
  providers: [Logger, CacheService, MetricsService],
  exports: [Logger, CacheService, MetricsService],
  global: true
})
export class CoreModule {}

Best Practices

1. Single Responsibility Modules

Each module should focus on a single business domain:

Typescript
// Recommended: Focused modules
ProductsModule   // Product catalog
OrdersModule     // Order processing
UsersModule      // User management
PaymentsModule   // Payment processing

// Avoid: Generic catch-all modules
ToolsModule      // Too broad
UtilsModule      // Unclear purpose

2. Encapsulate Business Logic in Services

Keep handlers thin; delegate logic to services:

Typescript
// Recommended
export class ProductsTools {
  constructor(private productService: ProductService) {}

  @Tool({ name: 'get_product' })
  async getProduct(input: { id: string }) {
    return this.productService.findById(input.id);
  }
}

// Avoid: Logic in handler
export class ProductsTools {
  @Tool({ name: 'get_product' })
  async getProduct(input: { id: string }) {
    const db = getDatabase();
    const result = await db.query('SELECT * FROM products WHERE id = $1', [input.id]);
    if (!result.rows[0]) throw new Error('Not found');
    return result.rows[0];
  }
}

3. Explicit Exports

Only export what other modules need:

Typescript
@Module({
  providers: [ProductService, ProductValidator, ProductMapper],
  exports: [ProductService]  // Only expose the service, not internal helpers
})
export class ProductsModule {}

4. Use ConfigService for Environment Variables

Never access process.env directly in services:

Typescript
// Recommended
@Injectable()
export class PaymentService {
  constructor(private config: ConfigService) {}

  private getApiKey(): string {
    return this.config.get('PAYMENT_API_KEY');
  }
}

// Avoid
@Injectable()
export class PaymentService {
  private apiKey = process.env.PAYMENT_API_KEY;  // Not testable
}

5. Consistent Module Structure

Follow a consistent file naming convention:

module-name/
├── module-name.module.ts      # Module definition
├── module-name.tools.ts       # Tool handlers
├── module-name.resources.ts   # Resource handlers
├── module-name.prompts.ts     # Prompt handlers
├── module-name.service.ts     # Business logic
├── module-name.repository.ts  # Data access (optional)
└── dto/                       # Data transfer objects
    ├── create-*.dto.ts
    └── update-*.dto.ts