DocumentationSdk

SDK Reference for Coding Agents

The full TypeScript SDK on one page, written for AI coding agents

Comprehensive SDK reference for AI agents editing NitroStack code

Architecture Overview

NitroStack uses decorator-based architecture inspired by NestJS:

  • Declarative - Use decorators instead of factory functions
  • Modular - Organize code into feature modules
  • DI-First - Dependency injection for testability
  • Type-Safe - Zod schemas for runtime validation
  • Protocol-Native - Built for MCP protocol, not HTTP

Key Principles

  1. Use decorators (@Tool, @Controller, @Module, @Injectable)
  2. No manual registration (server.tool(), server.resource())
  3. Constructor injection for dependencies
  4. Services contain business logic, tools/controllers are thin
  5. ES modules with .js extensions in imports

Application Bootstrap

Root Module (app.module.ts)

import { McpApp, Module, ConfigModule, JWTModule, BM25SearchTransform } from '@nitrostack/core';
import { ProductsModule } from './modules/products/products.module.js';
import { OrdersModule } from './modules/orders/orders.module.js';
import { DatabaseService } from './services/database.service.js';

@McpApp({
  module: AppModule,
  server: {
    name: 'my-ecommerce-server',
    version: '1.0.0',
    description: 'E-commerce MCP server'
  },
  transforms: [
    new BM25SearchTransform({
      defaultLimit: 5,
      alwaysVisible: ['auth_login', 'system_status']
    })
  ],
  spillover: {
    maxPayloadBytes: 10240,
    previewItems: 3
  },
  logging: {
    level: 'info'  // debug | info | warn | error
  }
})
@Module({
  imports: [
    ConfigModule.forRoot(),      // Environment variables
    JWTModule.forRoot(),         // JWT authentication
    ProductsModule,              // Feature modules
    OrdersModule
  ],
  providers: [DatabaseService],  // Global services
  controllers: []                // Global controllers
})
export class AppModule {}

Entry Point (index.ts)

import { McpApplicationFactory } from '@nitrostack/core';
import { AppModule } from './app.module.js';

// Bootstrap application
async function bootstrap() {
  const server = await McpApplicationFactory.create(AppModule);
  await server.start();
}

bootstrap().catch((error) => {
  console.error('❌ Failed to start server:', error);
  process.exit(1);
});

That's it! No manual server setup needed.

Modules

Modules organize related features into cohesive units.

Basic Module

import { Module } from '@nitrostack/core';
import { ProductsTools } from './products.tools.js';
import { ProductsResources } from './products.resources.js';
import { ProductsPrompts } from './products.prompts.js';
import { ProductService } from './products.service.js';

@Module({
  name: 'products',
  description: 'Product catalog management',
  controllers: [ProductsTools, ProductsResources, ProductsPrompts],
  providers: [ProductService],
  imports: [],      // Other modules this depends on
  exports: []       // Services to expose to other modules
})
export class ProductsModule {}

Module with Dependencies

@Module({
  name: 'orders',
  controllers: [OrdersTools],
  providers: [OrderService],
  imports: [ProductsModule, UserModule],  // Import other modules
  exports: [OrderService]                 // Make OrderService available to others
})
export class OrdersModule {}

Dynamic Modules (Advanced)

// Config module with options
@Module({})
export class ConfigModule {
  static forRoot(options?: ConfigOptions) {
    return {
      module: ConfigModule,
      providers: [
        {
          provide: 'CONFIG_OPTIONS',
          useValue: options || {}
        },
        ConfigService
      ],
      exports: [ConfigService]
    };
  }
}

Controllers & Namespacing

Controllers organize related tools, resources, and prompts under a shared namespace prefix. All tool and resource names defined inside a controller are prefixed automatically with ${prefix}_.

Controller Namespacing Example

import { Controller, Tool, InitialTool, ExecutionContext } from '@nitrostack/core';
import { z } from 'zod';

@Controller('finance')
export class FinanceController {
  // Registered in MCP protocol as 'finance_get_ledger'
  @Tool({
    name: 'get_ledger',
    description: 'Fetch general ledger records',
    inputSchema: z.object({
      accountId: z.string().describe('Target account ID')
    }),
  })
  async getLedger(input: { accountId: string }, ctx: ExecutionContext) {
    return { accountId: input.accountId, balance: 1000 };
  }

  // Registered as 'finance_bootstrap_session' and executed automatically on startup
  @InitialTool({
    name: 'bootstrap_session',
    description: 'Initial finance workspace setup tool',
  })
  async bootstrap(input: any, ctx: ExecutionContext) {
    return { status: 'ready', initializedAt: new Date().toISOString() };
  }
}

Registering Controllers in Modules

import { Module } from '@nitrostack/core';
import { FinanceController } from './finance.controller.js';
import { FinanceService } from './finance.service.js';

@Module({
  name: 'finance',
  controllers: [FinanceController],
  providers: [FinanceService],
  exports: [FinanceService],
})
export class FinanceModule {}

Tools

Tools are functions that AI models can call.

Basic Tool

import { ToolDecorator as Tool, ExecutionContext, z } from '@nitrostack/core';

export class ProductsTools {
  @Tool({
    name: 'get_product',
    description: 'Get product details by ID',
    inputSchema: z.object({
      product_id: z.string().describe('Unique product identifier')
    })
  })
  async getProduct(input: any, ctx: ExecutionContext) {
    ctx.logger.info('Fetching product', { id: input.product_id });
    
    return {
      id: input.product_id,
      name: 'Example Product',
      price: 99.99
    };
  }
}

Tool with Examples

@Tool({
  name: 'browse_products',
  description: 'Browse products with filters',
  inputSchema: z.object({
    category: z.string().optional().describe('Product category'),
    page: z.number().default(1).describe('Page number'),
    limit: z.number().default(10).describe('Items per page')
  }),
  examples: {
    request: { category: 'electronics', page: 1, limit: 10 },
    response: {
      products: [
        { id: '1', name: 'Laptop', price: 999 },
        { id: '2', name: 'Mouse', price: 29 }
      ],
      total: 2,
      page: 1
    }
  }
})
async browseProducts(input: any, ctx: ExecutionContext) {
  return {
    products: [],
    total: 0,
    page: input.page
  };
}

Tool with Widget

import { ToolDecorator as Tool, Widget, ExecutionContext } from '@nitrostack/core';

@Tool({
  name: 'get_product',
  description: 'Get product details',
  inputSchema: z.object({ product_id: z.string() }),
  examples: {
    request: { product_id: 'prod-1' },
    response: { id: 'prod-1', name: 'Laptop', price: 999, image_url: '/laptop.jpg' }
  }
})
@Widget('product-card')  // Link to src/widgets/app/product-card/page.tsx
async getProduct(input: any, ctx: ExecutionContext) {
  return await this.productService.findById(input.product_id);
}

Tool with Guards

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

@Tool({
  name: 'create_order',
  description: 'Create a new order',
  inputSchema: z.object({
    items: z.array(z.object({
      product_id: z.string(),
      quantity: z.number()
    }))
  })
})
@UseGuards(JWTGuard)  // Require authentication
async createOrder(input: any, ctx: ExecutionContext) {
  const userId = ctx.auth?.subject;  // Set by guard
  ctx.logger.info('Creating order', { userId, items: input.items });
  
  return await this.orderService.create(userId, input.items);
}

Tool with Caching

import { Tool, Cache } from '@nitrostack/core';

@Tool({
  name: 'get_categories',
  description: 'Get all product categories',
  inputSchema: z.object({})
})
@Cache({ ttl: 300 })  // Cache for 5 minutes
async getCategories() {
  return await this.productService.getCategories();
}

Tool with Rate Limiting

import { Tool, RateLimit } from '@nitrostack/core';

@Tool({
  name: 'send_email',
  description: 'Send email notification',
  inputSchema: z.object({
    to: z.string().email(),
    subject: z.string(),
    body: z.string()
  })
})
@RateLimit({
  requests: 10,           // Max requests
  window: '1m',           // Time window (1m, 1h, 1d)
  key: (ctx) => ctx.auth?.subject || 'anonymous'
})
async sendEmail(input: any, ctx: ExecutionContext) {
  await this.emailService.send(input.to, input.subject, input.body);
  return { success: true };
}

Tool with DI

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

@Injectable()
export class ProductService {
  constructor(private db: DatabaseService) {}
  
  async findById(id: string) {
    return this.db.queryOne('SELECT * FROM products WHERE id = ?', [id]);
  }
}

export class ProductsTools {
  constructor(private productService: ProductService) {}  // Auto-injected
  
  @Tool({ name: 'get_product' })
  async getProduct(input: any) {
    return await this.productService.findById(input.product_id);
  }
}

Resources

Resources are data schemas AI can read.

Basic Resource

import { ResourceDecorator as Resource, ExecutionContext } from '@nitrostack/core';

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

Resource with Widget

@Resource({
  uri: 'catalog://categories',
  name: 'Product Categories',
  description: 'All available product categories',
  mimeType: 'application/json',
  examples: {
    response: {
      categories: ['Electronics', 'Fashion', 'Home']
    }
  }
})
@Widget('categories-list')
async getCategoriesResource(uri: string, ctx: ExecutionContext) {
  const categories = await this.productService.getCategories();
  
  return {
    contents: [{
      uri,
      mimeType: 'application/json',
      text: JSON.stringify({ categories })
    }]
  };
}

Prompts

Prompts are conversation templates for AI.

Basic Prompt

import { PromptDecorator as Prompt, ExecutionContext } from '@nitrostack/core';

@Prompt({
  name: 'product_review',
  description: 'Generate product review template',
  arguments: [
    {
      name: 'product_id',
      description: 'ID of product to review',
      required: true
    },
    {
      name: 'rating',
      description: 'Rating (1-5)',
      required: false
    }
  ]
})
async getReviewPrompt(args: any, ctx: ExecutionContext) {
  const product = await this.productService.findById(args.product_id);
  
  return {
    messages: [
      {
        role: 'user',
        content: {
          type: 'text',
          text: `Write a ${args.rating || 5}-star review for: ${product.name}`
        }
      }
    ]
  };
}

Prompt with Context

@Prompt({
  name: 'order_summary',
  description: 'Generate order summary for customer',
  arguments: [
    { name: 'order_id', description: 'Order ID', required: true }
  ]
})
async getOrderSummaryPrompt(args: any, ctx: ExecutionContext) {
  const order = await this.orderService.findById(args.order_id);
  
  return {
    messages: [
      {
        role: 'system',
        content: {
          type: 'text',
          text: 'You are a helpful customer service assistant.'
        }
      },
      {
        role: 'user',
        content: {
          type: 'text',
          text: `Summarize order #${order.id} with ${order.items.length} items, total $${order.total}`
        }
      }
    ]
  };
}

Transforms Pipeline

Transforms optimize tool visibility, progressive discovery, and code execution at the MCP protocol level before tools reach the model.

Progressive Discovery with BM25 (BM25SearchTransform)

Hides exhaustive tool definitions behind a single search_tools meta-tool, dynamically exposing relevant tools on demand while preserving critical bootstrap tools:

import { BM25SearchTransform } from '@nitrostack/core';

@McpApp({
  module: AppModule,
  server: { name: 'enterprise-server', version: '1.0.0' },
  transforms: [
    new BM25SearchTransform({
      defaultLimit: 5,                                 // Max tools returned per search
      alwaysVisible: ['auth_login', 'system_status'],  // Tools never hidden
    }),
  ],
})
export class AppModule {}
  • Meta-Tool Injected: search_tools({ query: string, limit?: number })
  • Token Reduction: Decreases prompt context token usage by up to 98% in large toolsets.

Sandboxed Code Mode (CodeModeTransform)

Executes batch data processing scripts securely within isolated QuickJS WebAssembly worker threads:

import { CodeModeTransform } from '@nitrostack/core';

@McpApp({
  module: AppModule,
  server: { name: 'code-mode-server', version: '1.0.0' },
  transforms: [
    new CodeModeTransform({
      workerPoolSize: 4,                        // Concurrent WASM worker threads
      memoryLimitMb: 128,                       // RAM cap per VM instance
      timeoutMs: 15000,                         // Hard execution timeout
      alwaysVisible: ['data_aggregate_sum'],    // Regular tools kept visible
    }),
  ],
})
export class AppModule {}
  • Meta-Tools Injected:
    • search({ query: string, limit?: number }) - Discover available SDK API tools
    • get_schema({ toolName: string }) - Inspect input/output schema for code generation
    • execute({ code: string }) - Execute JavaScript code invoking Nitro tools in a sandbox

Guards

Guards implement authentication/authorization.

JWT Guard

import { Guard, ExecutionContext, Injectable } from '@nitrostack/core';
import jwt from 'jsonwebtoken';

@Injectable()
export class JWTGuard implements Guard {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const authHeader = context.metadata?.authorization;
    
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return false;
    }
    
    const token = authHeader.substring(7);
    
    try {
      const payload = jwt.verify(token, process.env.JWT_SECRET!);
      
      // Attach auth info to context
      context.auth = {
        subject: payload.sub,
        email: payload.email,
        ...payload
      };
      
      return true;
    } catch (error) {
      context.logger.warn('JWT verification failed', { error });
      return false;
    }
  }
}

Admin Guard

@Injectable()
export class AdminGuard implements Guard {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    // Assumes JWTGuard ran first
    const user = context.auth;
    
    if (!user) {
      return false;
    }
    
    // Check if user is admin
    return user.role === 'admin';
  }
}

// Usage: Multiple guards (all must pass)
@Tool({ name: 'delete_user' })
@UseGuards(JWTGuard, AdminGuard)
async deleteUser(input: any, ctx: ExecutionContext) {
  // Only admins with valid JWT can call this
}

Middleware

Middleware runs before/after tool execution.

Logging Middleware

import { Middleware, MiddlewareInterface, ExecutionContext } from '@nitrostack/core';

@Middleware()
export class LoggingMiddleware implements MiddlewareInterface {
  async use(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
    const start = Date.now();
    
    context.logger.info('Tool starting', {
      tool: context.toolName,
      input: context.input
    });
    
    try {
      const result = await next();
      
      const duration = Date.now() - start;
      context.logger.info('Tool completed', {
        tool: context.toolName,
        duration: `${duration}ms`
      });
      
      return result;
    } catch (error) {
      const duration = Date.now() - start;
      context.logger.error('Tool failed', {
        tool: context.toolName,
        duration: `${duration}ms`,
        error
      });
      throw error;
    }
  }
}

// Usage
@Tool({ name: 'my_tool' })
@UseMiddleware(LoggingMiddleware)
async myTool(input: any, ctx: ExecutionContext) {}

Auth Middleware

@Middleware()
export class AuthMiddleware implements MiddlewareInterface {
  async use(context: ExecutionContext, next: () => Promise<any>) {
    if (!context.auth) {
      throw new Error('Unauthorized');
    }
    
    return next();
  }
}

Interceptors & Data Spillover

Interceptors intercept and transform execution input or output streams.

Transform Interceptor

import { Interceptor, InterceptorInterface, ExecutionContext, UseInterceptors } from '@nitrostack/core';

@Interceptor()
export class TransformInterceptor implements InterceptorInterface {
  async intercept(context: ExecutionContext, next: () => Promise<any>): Promise<any> {
    const result = await next();
    
    // Wrap all responses in standard format
    return {
      success: true,
      data: result,
      timestamp: new Date().toISOString(),
      tool: context.toolName
    };
  }
}

// Usage
@Tool({ name: 'get_product' })
@UseInterceptors(TransformInterceptor)
async getProduct(input: any) {
  return { id: '1', name: 'Product' };
}

Data Spillover Interceptor (DataSpilloverInterceptor)

Automatically intercepts large tool responses and diverts the full payload to a temporary file while returning a truncated preview to the LLM to avoid context window overflow:

import { Tool, UseInterceptors, DataSpilloverInterceptor, ExecutionContext } from '@nitrostack/core';

@Tool({
  name: 'dump_records',
  description: 'Fetch high-volume financial transaction history',
})
@UseInterceptors(new DataSpilloverInterceptor({
  maxPayloadBytes: 10240, // 10KB threshold
  previewItems: 3         // Retain first 3 items in preview
}))
async dumpRecords(input: any, ctx: ExecutionContext) {
  return await this.service.fetchMassiveData();
}

Spillover Response Structure:

{
  "_spillover": true,
  "file": "/tmp/spillover-a1b2c3d4.json",
  "totalItems": 1500,
  "byteSize": 412850,
  "preview": [{ "id": 1 }, { "id": 2 }, { "id": 3 }],
  "message": "Payload exceeded 10240 bytes. Full data persisted to disk."
}

Pipes

Pipes transform/validate input before handler execution.

Validation Pipe

import { Pipe, PipeInterface } from '@nitrostack/core';
import { z } from 'zod';

@Pipe()
export class ValidationPipe implements PipeInterface {
  async transform(value: any, metadata: any): Promise<any> {
    if (metadata.schema) {
      // Validate with Zod
      return metadata.schema.parse(value);
    }
    return value;
  }
}

Services & Dependency Injection

Injectable Service

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

@Injectable()
export class ProductService {
  constructor(private db: DatabaseService) {}  // DI
  
  async findById(id: string) {
    return this.db.queryOne('SELECT * FROM products WHERE id = ?', [id]);
  }
  
  async search(query: string) {
    return this.db.query(
      'SELECT * FROM products WHERE name LIKE ?',
      [`%${query}%`]
    );
  }
  
  async getCategories() {
    return this.db.query('SELECT DISTINCT category FROM products');
  }
}

Using Services in Tools

export class ProductsTools {
  constructor(
    private productService: ProductService,
    private cacheService: CacheService
  ) {}  // Both auto-injected
  
  @Tool({ name: 'search_products' })
  async searchProducts(input: any, ctx: ExecutionContext) {
    const results = await this.productService.search(input.query);
    await this.cacheService.set(`search:${input.query}`, results);
    return results;
  }
}

Decorators Reference

Core Decorators

DecoratorPurposeExample
@McpApp(options)Root application module@McpApp({ server: { name: 'my-server' } })
@Module(options)Feature module@Module({ name: 'products', controllers: [...] })
@Controller(prefix)Namespace prefix for tools and resources@Controller('finance') class FinanceController {}
@Injectable()Mark for DI@Injectable() class ProductService {}

Tool Decorators

DecoratorPurposeExample
@Tool(options)Define tool@Tool({ name: 'get_product', inputSchema: z.object({}) })
@InitialTool(options)Auto-executed initial setup tool@InitialTool({ name: 'bootstrap_session' })
@Resource(options)Define resource@Resource({ uri: 'product://{id}' })
@Prompt(options)Define prompt@Prompt({ name: 'review_prompt' })

Feature Decorators

DecoratorPurposeExample
@Widget(name)Link UI widget@Widget('product-card')
@UseGuards(...guards)Apply guards@UseGuards(JWTGuard, AdminGuard)
@UseMiddleware(...mw)Apply middleware@UseMiddleware(LoggingMiddleware)
@UseInterceptors(...int)Apply interceptors@UseInterceptors(TransformInterceptor)
@Cache(options)Cache responses@Cache({ ttl: 300 })
@RateLimit(options)Rate limiting@RateLimit({ requests: 10, window: '1m' })
@HealthCheck(name)Define health check@HealthCheck('database')

MCP Tasks

MCP Tasks allow for long-running, asynchronous operations with progress updates and cancellation.

Enabling Tasks

Set taskSupport to 'optional' or 'required' (defaults to 'forbidden').

@Tool({
  name: 'audit_data',
  description: 'Audits large datasets (asynchronous)',
  taskSupport: 'optional', // or 'required'
  inputSchema: z.object({ datasetId: z.string() })
})
async auditData(input: any, ctx: ExecutionContext) {
  // Use ctx.task helpers
  if (ctx.task) {
    ctx.task.updateProgress('Scanning headers...');
    ctx.task.throwIfCancelled();
  }
  return { results: [] };
}

Task Helpers (ExecutionContext.task)

  • task.updateProgress(message: string) - Sends a status update to the client.
  • task.requestInput(message: string) - Transition task to input_required state.
  • task.throwIfCancelled() - Throws TaskCancelledError if client requested cancellation.
  • task.isCancelled - Boolean flag for cooperative cancellation checks.
  • task.taskId - Unique ID for the current task.

Multi Round-Trip Requests (MRTR)

SEP-2322 compliant interactive conversational workflows requiring client confirmation or step-by-step inputs before proceeding:

import { Tool, ExecutionContext, inputRequired, acceptedContent } from '@nitrostack/core';
import { z } from 'zod';

@Tool({
  name: 'transfer_funds',
  description: 'Initiate bank transfer with interactive user confirmation',
  inputSchema: z.object({
    recipientId: z.string(),
    amount: z.number().positive(),
  }),
})
async transferFunds(input: { recipientId: string; amount: number }, ctx: ExecutionContext) {
  // Check if confirmation was provided in previous round-trip
  if (!acceptedContent(ctx.metadata?.inputResponses, 'confirm')) {
    return inputRequired({
      requests: [
        {
          id: 'confirm',
          message: `Confirm transfer of $${input.amount} to account ${input.recipientId}?`,
          schema: { type: 'boolean' },
        },
      ],
      requestState: {
        recipientId: input.recipientId,
        amount: input.amount,
        step: 'awaiting_confirmation',
      },
    });
  }

  // Confirmation received; proceed with transfer
  return {
    status: 'success',
    transactionId: 'tx_998877',
    transferred: input.amount,
  };
}

Modern Protocol (MCP 2026-07-28 Dual-Spec)

NitroStack natively supports both modern MCP 2026-07-28 (Streamable HTTP on /mcp) and legacy SSE (/sse):

import {
  MODERN_PROTOCOL_VERSION, // '2026-07-28'
  LEGACY_PROTOCOL_VERSION, // '2025-06-18'
  PROTOCOL_ENV_VAR,        // 'NITRO_MCP_PROTOCOL_VERSION'
  inputRequired,           // Multi Round-Trip Request helper
  acceptedContent,
  EXT_APP,                 // 'io.modelcontextprotocol/app'
  EXT_TASKS,               // 'io.modelcontextprotocol/tasks'
} from '@nitrostack/core';
  • Protocol Negotiation: Default era is auto (routes modern clients to Streamable HTTP /mcp while transparently supporting legacy SSE /sse).
  • Multi Round-Trip Requests (MRTR): Return inputRequired({ ... }) for interactive conversational tool steps.
  • Client ID Metadata Documents (CIMD): Modern replacement for OAuth dynamic client registration (RFC 7591) via createClientIdMetadataDocument.

Widgets

UI components rendered alongside tool responses.

Widget File Structure

src/widgets/
├── app/
│   ├── product-card/
│   │   └── page.tsx          # Widget component
│   ├── products-grid/
│   │   └── page.tsx
│   └── ...
├── types/
│   └── tool-data.ts           # Generated types
├── styles/
│   └── ecommerce.ts           # Shared inline styles
├── widget-manifest.json       # Widget metadata
├── package.json
└── next.config.js

Basic Widget

'use client';

import { useEffect, useState } from 'react';

export default function ProductCard() {
  const [data, setData] = useState<any>(null);

  useEffect(() => {
    // Listen for data from Studio/MCP
    const handleMessage = (event: MessageEvent) => {
      if (event.data.type === 'toolOutput') {
        setData(event.data.data);
      }
    };

    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
  }, []);

  if (!data) {
    return <div>Loading...</div>;
  }

  return (
    <div style={{ padding: '20px', border: '1px solid #ddd' }}>
      <h2>{data.name}</h2>
      <p>Price: ${data.price}</p>
      <img src={data.image_url} alt={data.name} style={{ maxWidth: '200px' }} />
    </div>
  );
}

Widget with Types

'use client';

import { useEffect, useState } from 'react';
import { GetProductOutput } from '../../types/tool-data';

export default function ProductCard() {
  const [data, setData] = useState<GetProductOutput | null>(null);

  useEffect(() => {
    const handleMessage = (event: MessageEvent) => {
      if (event.data.type === 'toolOutput') {
        setData(event.data.data as GetProductOutput);
      }
    };

    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
  }, []);

  if (!data) return <div>Loading...</div>;

  // TypeScript knows data.name, data.price, etc.
  return (
    <div>
      <h2>{data.name}</h2>
      <p>${data.price}</p>
    </div>
  );
}

Widget Manifest

{
  "widgets": [
    {
      "uri": "product-card",
      "name": "Product Card",
      "description": "Display product details",
      "toolName": "get_product",
      "examples": {
        "request": { "product_id": "1" },
        "response": { "id": "1", "name": "Laptop", "price": 999 }
      }
    }
  ]
}

Linking Widget to Tool

@Tool({
  name: 'get_product',
  inputSchema: z.object({ product_id: z.string() })
})
@Widget('product-card')  // Links to src/widgets/app/product-card/page.tsx
async getProduct(input: any) {
  return { id: input.product_id, name: 'Laptop', price: 999 };
}

Health Checks

Monitor system health.

Basic Health Check

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

@Injectable()
export class SystemHealthCheck {
  @HealthCheck('system')
  async checkSystem() {
    const uptime = process.uptime();
    const memory = process.memoryUsage();
    
    return {
      status: uptime > 0 ? 'up' : 'down',
      message: 'System is operational',
      details: {
        uptime: `${Math.floor(uptime)}s`,
        memory: `${Math.round(memory.heapUsed / 1024 / 1024)}MB`
      }
    };
  }
}

Database Health Check

@Injectable()
export class DatabaseHealthCheck {
  constructor(private db: DatabaseService) {}
  
  @HealthCheck('database')
  async checkDatabase() {
    try {
      await this.db.query('SELECT 1');
      return {
        status: 'up',
        message: 'Database is responsive',
        details: { connection: 'active' }
      };
    } catch (error) {
      return {
        status: 'down',
        message: 'Database connection failed',
        details: { error: error.message }
      };
    }
  }
}

Caching

Cache tool responses for performance.

import { Tool, Cache } from '@nitrostack/core';

// Simple TTL caching
@Tool({ name: 'get_categories' })
@Cache({ ttl: 300 })  // 5 minutes
async getCategories() {
  return await this.productService.getCategories();
}

// Custom cache key
@Tool({ name: 'get_product' })
@Cache({
  ttl: 60,
  key: (input) => `product:${input.product_id}`
})
async getProduct(input: any) {
  return await this.productService.findById(input.product_id);
}

Rate Limiting

Limit tool execution frequency.

import { Tool, RateLimit } from '@nitrostack/core';

// Per-user rate limiting
@Tool({ name: 'send_email' })
@RateLimit({
  requests: 10,                              // Max 10 requests
  window: '1m',                              // Per 1 minute
  key: (ctx) => ctx.auth?.subject || 'anon' // Key by user ID
})
async sendEmail(input: any) {
  await this.emailService.send(input.to, input.subject, input.body);
  return { success: true };
}

// Global rate limiting
@Tool({ name: 'expensive_operation' })
@RateLimit({
  requests: 100,
  window: '1h',
  key: () => 'global'
})
async expensiveOperation(input: any) {
  // ...
}

Error Handling & Events

Error Handling

@Tool({ name: 'get_user' })
async getUser(input: any, ctx: ExecutionContext) {
  const user = await this.userService.findById(input.user_id);
  
  if (!user) {
    throw new Error('User not found');
  }
  
  return user;
}

// Custom error class
export class NotFoundError extends Error {
  constructor(resource: string, id: string) {
    super(`${resource} with ID ${id} not found`);
    this.name = 'NotFoundError';
  }
}

@Tool({ name: 'get_product' })
async getProduct(input: any) {
  const product = await this.productService.findById(input.product_id);
  
  if (!product) {
    throw new NotFoundError('Product', input.product_id);
  }
  
  return product;
}

Event Emission

Do NOT use ctx.emit(). Use top-level emitEvent() from @nitrostack/core:

import { emitEvent, ExecutionContext, Tool } from '@nitrostack/core';

@Tool({ name: 'create_order' })
async createOrder(input: any, ctx: ExecutionContext) {
  const order = await this.orderService.create(input);

  // Emit event asynchronously across listeners
  emitEvent('order.created', {
    orderId: order.id,
    userId: ctx.auth?.subject,
    timestamp: new Date().toISOString(),
  });

  return order;
}

File Structure

src/
├── modules/
│   ├── auth/
│   │   ├── auth.module.ts
│   │   ├── auth.tools.ts
│   │   ├── auth.resources.ts
│   │   ├── auth.prompts.ts
│   │   ├── auth.service.ts
│   │   └── guards/
│   │       └── jwt.guard.ts
│   ├── products/
│   │   ├── products.module.ts
│   │   ├── products.tools.ts
│   │   ├── products.resources.ts
│   │   ├── products.prompts.ts
│   │   └── products.service.ts
│   └── ...
├── services/
│   ├── database.service.ts
│   └── cache.service.ts
├── guards/
│   ├── jwt.guard.ts
│   └── admin.guard.ts
├── middleware/
│   └── logging.middleware.ts
├── interceptors/
│   └── transform.interceptor.ts
├── pipes/
│   └── validation.pipe.ts
├── health/
│   ├── system.health.ts
│   └── database.health.ts
├── widgets/
│   ├── app/
│   │   ├── product-card/
│   │   ├── products-grid/
│   │   └── ...
│   ├── types/
│   │   └── tool-data.ts
│   └── styles/
│       └── shared.ts
├── app.module.ts
└── index.ts

Import Rules

Always Use .js Extensions

// Correct
import { ProductService } from './products.service.js';
import { JWTGuard } from '../auth/jwt.guard.js';
import { DatabaseService } from '../../services/database.service.js';

// Wrong
import { ProductService } from './products.service';

Core Imports

// Core decorators & runtime
import {
  McpApp,
  Module,
  Controller,
  Tool,
  InitialTool,
  Resource,
  Prompt,
  Injectable,
  UseGuards,
  UseMiddleware,
  UseInterceptors,
  Cache,
  RateLimit,
  HealthCheck,
  ExecutionContext,
  emitEvent,
} from '@nitrostack/core';

// Protocol & MRTR helpers
import {
  inputRequired,
  acceptedContent,
  MODERN_PROTOCOL_VERSION,
  LEGACY_PROTOCOL_VERSION,
} from '@nitrostack/core';

// Transforms & Interceptors
import {
  BM25SearchTransform,
  CodeModeTransform,
  VisibilityTransform,
  DataSpilloverInterceptor,
} from '@nitrostack/core';

// Config & Auth Modules (exported directly from @nitrostack/core)
import {
  ConfigModule,
  ConfigService,
  JWTModule,
  OAuthModule,
  ApiKeyModule,
} from '@nitrostack/core';

// Validation
import { z } from 'zod';

// Widgets (in widget files)
import { withToolData } from '@nitrostack/widgets';

Common Patterns

CRUD Operations

export class ProductsTools {
  constructor(private productService: ProductService) {}
  
  // Create
  @Tool({
    name: 'create_product',
    inputSchema: z.object({
      name: z.string(),
      price: z.number(),
      category: z.string()
    })
  })
  @UseGuards(JWTGuard, AdminGuard)
  async create(input: any) {
    return await this.productService.create(input);
  }
  
  // Read
  @Tool({
    name: 'get_product',
    inputSchema: z.object({ product_id: z.string() })
  })
  @Cache({ ttl: 60 })
  async get(input: any) {
    return await this.productService.findById(input.product_id);
  }
  
  // Update
  @Tool({
    name: 'update_product',
    inputSchema: z.object({
      product_id: z.string(),
      name: z.string().optional(),
      price: z.number().optional()
    })
  })
  @UseGuards(JWTGuard, AdminGuard)
  async update(input: any) {
    return await this.productService.update(input.product_id, input);
  }
  
  // Delete
  @Tool({
    name: 'delete_product',
    inputSchema: z.object({ product_id: z.string() })
  })
  @UseGuards(JWTGuard, AdminGuard)
  async delete(input: any) {
    await this.productService.delete(input.product_id);
    return { success: true };
  }
}

Pagination Pattern

@Tool({
  name: 'list_products',
  inputSchema: z.object({
    page: z.number().default(1),
    limit: z.number().default(20).max(100),
    category: z.string().optional()
  })
})
async listProducts(input: any) {
  const offset = (input.page - 1) * input.limit;
  
  const products = await this.productService.list({
    limit: input.limit,
    offset,
    category: input.category
  });
  
  const total = await this.productService.count({ category: input.category });
  
  return {
    products,
    pagination: {
      page: input.page,
      limit: input.limit,
      total,
      pages: Math.ceil(total / input.limit)
    }
  };
}

Search Pattern

@Tool({
  name: 'search_products',
  inputSchema: z.object({
    query: z.string().min(1),
    filters: z.object({
      category: z.string().optional(),
      min_price: z.number().optional(),
      max_price: z.number().optional()
    }).optional()
  })
})
@Cache({ ttl: 30, key: (input) => `search:${input.query}:${JSON.stringify(input.filters)}` })
async search(input: any) {
  return await this.productService.search(input.query, input.filters);
}

Key Rules for AI Agents

  1. Always use decorators - Use @Tool, @Controller, @Module, @Injectable (no factory functions)
  2. Constructor injection - Never use new ClassName() for services; use DI
  3. Services for logic - Tools and controllers should remain thin wrappers
  4. Zod for schemas - All tool inputs must have inputSchema defined
  5. Return JSON - All tool outputs must be JSON-serializable
  6. ES modules - Always use .js in imports, never .ts
  7. ExecutionContext - Handler context parameter (provides logger, auth, verifiedSubject, task, abortSignal)
  8. Never use ctx.emit() - Always import and call emitEvent('event.name', payload) from @nitrostack/core
  9. Spillover for large payloads - Use DataSpilloverInterceptor when tool output may exceed LLM context bounds
  10. Progressive discovery - Use BM25SearchTransform or CodeModeTransform for large tool surfaces

That's the complete NitroStack SDK reference!

For more details, check:

  • /docs/sdk/typescript/ - Full TypeScript SDK documentation
  • /templates/typescript-starter/ - Starter calculator example
  • /templates/typescript-enterprise-search/ - Enterprise search with BM25 progressive discovery
  • /templates/typescript-code-mode/ - Sandboxed QuickJS WASM code execution mode
  • /templates/typescript-pizzaz/ - Rich interactive map and UI widgets example
  • /templates/typescript-oauth/ - OAuth 2.1 authentication flight booking example

On this page