@roostjs/schema Guides

Task-oriented instructions for defining tool input schemas, optional fields, nested objects, and descriptions.

How to define a tool input schema

Return a record of field names to schema builders from the schema() method in any AI tool or MCP tool. All fields are required by default.

import { type Tool, type ToolRequest } from '@roostjs/ai';
import { schema } from '@roostjs/schema';

export class SearchTool implements Tool {
  description(): string {
    return 'Search articles by keyword';
  }

  schema(s: typeof schema) {
    return {
      query: s.string().description('The search query'),
      limit: s.integer().min(1).max(50).description('Max results to return'),
      sortBy: s.enum(['relevance', 'date', 'views']).description('Result ordering'),
    };
  }

  async handle(request: ToolRequest): Promise<string> {
    const query = request.get<string>('query');
    const limit = request.get<number>('limit');
    const sortBy = request.get<'relevance' | 'date' | 'views'>('sortBy');

    const results = await searchArticles({ query, limit, sortBy });
    return JSON.stringify(results);
  }
}

How to use optional and nested schemas

Chain .optional() to make a field not required. Use s.object() to nest structured data.

import { schema } from '@roostjs/schema';

// Optional primitive
const s_optional_number = schema.integer().min(0).optional();

// Optional object
const s_filter = schema.object()
  .property('status', schema.enum(['active', 'inactive']), true)
  .property('createdAfter', schema.string().format('date'))
  .optional()
  .description('Optional filter criteria');

// In a tool schema method
schema(s: typeof schema) {
  return {
    query: s.string().description('Search query'),
    // Optional fields — AI will omit them if not needed
    page: s.integer().min(1).default(1).optional(),
    filter: s.object()
      .property('status', s.enum(['draft', 'published']), true)
      .optional()
      .description('Optional status filter'),
  };
}

Nested objects in tool schemas let the AI provide structured sub-parameters. Access them with request.get<T>('fieldName') where T is the inferred object type.

How to add descriptions to schema fields

Chain .description() on every field. The AI model uses these descriptions to decide what values to pass — clear descriptions reduce tool call errors.

import { schema } from '@roostjs/schema';

// Descriptions help the model understand intent and constraints
const createUserSchema = {
  name: schema.string()
    .description('Full name of the user (first and last)')
    .minLength(2)
    .maxLength(100),

  email: schema.string()
    .description('Valid email address — must be unique in the system')
    .format('email'),

  role: schema.enum(['user', 'admin', 'moderator'])
    .description('User role. Defaults to user for most signups; use admin for internal team members only')
    .default('user'),

  age: schema.integer()
    .description('Age in years. Must be 13 or older')
    .min(13)
    .optional(),
};

Good description guidelines: state the purpose, mention format constraints (ISO 8601, UUID, etc.), and call out non-obvious rules. Avoid restating the type — the model already knows email is a string.

// Too terse — model won't know the format
{ date: schema.string() }

// Good — explicit format and timezone expectation
{ date: schema.string().description('ISO 8601 date string (e.g. 2024-01-15). Use UTC.') }

// Too verbose — restates the type
{ count: schema.integer().description('An integer representing the count of items') }

// Good — states the constraint and purpose
{ count: schema.integer().description('Number of results to return. Max 100.').max(100) }