NitroStack SDK Reference - For AI Code Editors
Comprehensive SDK reference for AI agents editing NitroStack v3.0 code
Table of Contents
- Architecture Overview
- Application Bootstrap
- Modules
- Controllers & Namespacing
- Tools
- Resources
- Prompts
- Transforms Pipeline
- Guards
- Middleware
- Interceptors & Data Spillover
- Pipes
- Services & DI
- Decorators Reference
- MCP Tasks
- Multi Round-Trip Requests (MRTR)
- Modern Protocol (MCP 2026-07-28 Dual-Spec)
- Widgets
- Health Checks
- Caching
- Rate Limiting
- Error Handling & Events
- File Structure
- Import Rules
- Common Patterns
Architecture Overview
NitroStack v3.0 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
- Use decorators (
@Tool,@Controller,@Module,@Injectable) - No manual registration (
server.tool(),server.resource()) - Constructor injection for dependencies
- Services contain business logic, tools/controllers are thin
- ES modules with
.jsextensions 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 toolsget_schema({ toolName: string })- Inspect input/output schema for code generationexecute({ 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
| Decorator | Purpose | Example |
|---|---|---|
@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
| Decorator | Purpose | Example |
|---|---|---|
@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
| Decorator | Purpose | Example |
|---|---|---|
@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 toinput_requiredstate.task.throwIfCancelled()- ThrowsTaskCancelledErrorif 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/mcpwhile 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
- Always use decorators - Use
@Tool,@Controller,@Module,@Injectable(no factory functions) - Constructor injection - Never use
new ClassName()for services; use DI - Services for logic - Tools and controllers should remain thin wrappers
- Zod for schemas - All tool inputs must have
inputSchemadefined - Return JSON - All tool outputs must be JSON-serializable
- ES modules - Always use
.jsin imports, never.ts - ExecutionContext - Handler context parameter (provides
logger,auth,verifiedSubject,task,abortSignal) - Never use
ctx.emit()- Always import and callemitEvent('event.name', payload)from@nitrostack/core - Spillover for large payloads - Use
DataSpilloverInterceptorwhen tool output may exceed LLM context bounds - Progressive discovery - Use
BM25SearchTransformorCodeModeTransformfor 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