For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://modelgates.ai/docs/_mcp/server.

Skills Loader

Overview

This example shows how to build encapsulated, self-managing tools that inject domain-specific context into conversations. When a skill is loaded, it automatically enriches subsequent turns with specialized instructions.

Prerequisites

bash
pnpm add @modelgates/sdk zod

Create a skills directory:

bash
mkdir -p ~/.claude/skills/pdf-processingmkdir -p ~/.claude/skills/data-analysismkdir -p ~/.claude/skills/code-review

Basic Skills Tool

typescript
import { ModelGates, tool } from '@modelgates/agent';import { readFileSync, existsSync, readdirSync } from 'fs';import path from 'path';import { z } from 'zod'; const modelgates = new ModelGates({  apiKey: process.env.MODELGATES_API_KEY,}); const SKILLS_DIR = path.join(process.env.HOME || '~', '.claude', 'skills'); // List available skillsconst listAvailableSkills = (): string[] => {  if (!existsSync(SKILLS_DIR)) return [];  return readdirSync(SKILLS_DIR, { withFileTypes: true })    .filter((dirent) => dirent.isDirectory())    .filter((dirent) => existsSync(path.join(SKILLS_DIR, dirent.name, 'SKILL.md')))    .map((dirent) => dirent.name);}; const skillsTool = tool({  name: 'Skill',  description: `Load a specialized skill to enhance the assistant's capabilities.Available skills: ${listAvailableSkills().join(', ') || 'none configured'}Each skill provides domain-specific instructions and capabilities.`,   inputSchema: z.object({    type: z.string().describe("The skill type to load (e.g., 'pdf-processing')"),  }),   outputSchema: z.string(),   // This is where the magic happens - modify context for next turn  nextTurnParams: {    input: (params, context) => {      // Prevent duplicate skill loading      const skillMarker = `[Skill: ${params.type}]`;      if (JSON.stringify(context.input).includes(skillMarker)) {        return context.input;      }       // Load the skill's instructions      const skillPath = path.join(SKILLS_DIR, params.type, 'SKILL.md');      if (!existsSync(skillPath)) {        return context.input;      }       const skill = readFileSync(skillPath, 'utf-8');      const skillDir = path.join(SKILLS_DIR, params.type);       // Inject skill context into the conversation      const currentInput = Array.isArray(context.input) ? context.input : [context.input];       return [        ...currentInput,        {          role: 'user',          content: `${skillMarker}Base directory for this skill: $ $`,        },      ];    },  },   execute: async (params, context) => {    const skillMarker = `[Skill: ${params.type}]`;     // Check if already loaded    if (JSON.stringify(context?.turnRequest?.input || []).includes(skillMarker)) {      return `Skill ${params.type} is already loaded`;    }     const skillPath = path.join(SKILLS_DIR, params.type, 'SKILL.md');    if (!existsSync(skillPath)) {      const available = listAvailableSkills();      return `Skill "${params.type}" not found. Available skills: ${available.join(', ') || 'none'}`;    }     return `Launching skill ${params.type}`;  },});

Usage

typescript
const result = modelgates.callModel({  model: 'anthropic/claude-sonnet-4.5',  input: 'I need to process a PDF and extract tables from it',  tools: [skillsTool],}); const text = await result.getText();// The model will call the Skill tool, loading pdf-processing context// Subsequent responses will have access to the skill's instructions

Example Skill File

Create ~/.claude/skills/pdf-processing/SKILL.md:

markdown
# PDF Processing Skill You are now equipped with PDF processing capabilities. ## Available ToolsWhen processing PDFs, you have access to:- `extract_text`: Extract all text from a PDF- `extract_tables`: Extract tables as structured data- `extract_images`: Extract embedded images- `split_pdf`: Split PDF into individual pages ## Best Practices1. Always check PDF file size before processing2. For large PDFs (>50 pages), process in chunks3. OCR may be needed for scanned documents4. Tables may span multiple pages - handle accordingly ## Output Formats- Text: Plain text or markdown- Tables: JSON, CSV, or markdown tables- Images: PNG with sequential naming ## Error Handling- If a PDF is encrypted, request the password- If OCR fails, suggest alternative approaches- Report page numbers for any extraction errors

Extended: Multi-Skill Loader

Load multiple skills in a single call:

typescript
const multiSkillLoader = tool({  name: 'load_skills',  description: 'Load multiple skills at once for complex tasks',   inputSchema: z.object({    skills: z.array(z.string()).describe('Array of skill names to load'),  }),   outputSchema: z.object({    loaded: z.array(z.string()),    failed: z.array(      z.object({        name: z.string(),        reason: z.string(),      })    ),  }),   nextTurnParams: {    input: (params, context) => {      let newInput = Array.isArray(context.input) ? context.input : [context.input];       for (const skillName of params.skills) {        const skillMarker = `[Skill: ${skillName}]`;         // Skip if already loaded        if (JSON.stringify(newInput).includes(skillMarker)) {          continue;        }         const skillPath = path.join(SKILLS_DIR, skillName, 'SKILL.md');        if (!existsSync(skillPath)) {          continue;        }         const skillContent = readFileSync(skillPath, 'utf-8');        const skillDir = path.join(SKILLS_DIR, skillName);         newInput = [          ...newInput,          {            role: 'user',            content: `${skillMarker}Base directory: $ $`,          },        ];      }       return newInput;    },  },   execute: async ({ skills }) => {    const loaded: string[] = [];    const failed: Array<{ name: string; reason: string }> = [];     for (const skill of skills) {      const skillPath = path.join(SKILLS_DIR, skill, 'SKILL.md');      if (existsSync(skillPath)) {        loaded.push(skill);      } else {        failed.push({ name: skill, reason: 'Skill not found' });      }    }     return { loaded, failed };  },}); // Usageconst result = modelgates.callModel({  model: 'anthropic/claude-sonnet-4.5',  input: 'I need to analyze a PDF report and create visualizations',  tools: [multiSkillLoader],});// Model might call: load_skills({ skills: ['pdf-processing', 'data-analysis'] })

Extended: Skill with Options

Skills that accept configuration:

typescript
const configurableSkillLoader = tool({  name: 'configure_skill',  description: 'Load a skill with custom configuration options',   inputSchema: z.object({    skillName: z.string(),    options: z      .object({        verbosity: z.enum(['minimal', 'normal', 'detailed']).default('normal'),        strictMode: z.boolean().default(false),        outputFormat: z.enum(['json', 'markdown', 'plain']).default('markdown'),      })      .optional(),  }),   outputSchema: z.object({    status: z.enum(['loaded', 'already_loaded', 'not_found']),    message: z.string(),    configuration: z.record(z.unknown()).optional(),  }),   nextTurnParams: {    input: (params, context) => {      const skillMarker = `[Skill: ${params.skillName}]`;      if (JSON.stringify(context.input).includes(skillMarker)) {        return context.input;      }       const skillPath = path.join(SKILLS_DIR, params.skillName, 'SKILL.md');      if (!existsSync(skillPath)) {        return context.input;      }       const skillContent = readFileSync(skillPath, 'utf-8');      const options = params.options || {};       // Build configuration header      const configHeader = `## Skill Configuration- Verbosity: ${options.verbosity || 'normal'}- Strict Mode: ${options.strictMode || false}- Output Format: ${options.outputFormat || 'markdown'}`;       const currentInput = Array.isArray(context.input) ? context.input : [context.input];       return [        ...currentInput,        {          role: 'user',          content: `${skillMarker}$ $`,        },      ];    },     // Adjust model behavior based on skill    temperature: (params, context) => {      // Lower temperature for strict mode      if (params.options?.strictMode) {        return 0.3;      }      return context.temperature;    },  },   execute: async ({ skillName, options }) => {    const skillPath = path.join(SKILLS_DIR, skillName, 'SKILL.md');     if (!existsSync(skillPath)) {      return {        status: 'not_found' as const,        message: `Skill "${skillName}" not found`,      };    }     return {      status: 'loaded' as const,      message: `Skill "${skillName}" loaded with configuration`,      configuration: options || {},    };  },});

Skill Discovery Tool

List and describe available skills:

typescript
const skillDiscoveryTool = tool({  name: 'list_skills',  description: 'List all available skills with their descriptions',   inputSchema: z.object({    category: z.string().optional().describe('Filter by category'),  }),   outputSchema: z.object({    skills: z.array(      z.object({        name: z.string(),        description: z.string(),        hasConfig: z.boolean(),      })    ),    totalCount: z.number(),  }),   execute: async ({ category }) => {    const availableSkills = listAvailableSkills();    const skills = [];     for (const skillName of availableSkills) {      const skillPath = path.join(SKILLS_DIR, skillName, 'SKILL.md');      const content = readFileSync(skillPath, 'utf-8');       // Extract first paragraph as description      const lines = content.split('\n').filter((l) => l.trim());      const description = lines.find((l) => !l.startsWith('#')) || 'No description';       // Check for config file      const configPath = path.join(SKILLS_DIR, skillName, 'config.json');      const hasConfig = existsSync(configPath);       skills.push({        name: skillName,        description: description.slice(0, 100),        hasConfig,      });    }     return {      skills,      totalCount: skills.length,    };  },});

Complete Example

Putting it all together:

typescript
import { ModelGates, tool, stepCountIs } from '@modelgates/agent';import { readFileSync, existsSync, readdirSync } from 'fs';import path from 'path';import { z } from 'zod'; const modelgates = new ModelGates({  apiKey: process.env.MODELGATES_API_KEY,}); const SKILLS_DIR = path.join(process.env.HOME || '~', '.claude', 'skills'); // ... (include skillsTool, multiSkillLoader, skillDiscoveryTool from above) // Use all skill tools togetherconst result = modelgates.callModel({  model: 'anthropic/claude-sonnet-4.5',  input: `I have a complex task:1. First, show me what skills are available2. Load the appropriate skills for PDF analysis3. Then help me extract and analyze data from report.pdf`,  tools: [skillDiscoveryTool, skillsTool, multiSkillLoader],  stopWhen: stepCountIs(10),}); const text = await result.getText();console.log(text);

Key Patterns

1. Idempotency

Always check if a skill is already loaded:

typescript
nextTurnParams: {  input: (params, context) => {    const marker = `[Skill: ${params.type}]`;    if (JSON.stringify(context.input).includes(marker)) {      return context.input; // Don't add again    }    // ... add skill  },},

2. Graceful Fallbacks

Handle missing skills gracefully:

typescript
execute: async (params) => {  if (!existsSync(skillPath)) {    return `Skill not found. Available: ${listAvailableSkills().join(', ')}`;  }  // ...},

3. Context Preservation

Always preserve existing input:

typescript
nextTurnParams: {  input: (params, context) => {    const currentInput = Array.isArray(context.input)      ? context.input      : [context.input];    return [...currentInput, newMessage]; // Append, don't replace  },},

4. Clear Markers

Use unique markers to identify injected content:

typescript
const skillMarker = `[Skill: $]`;// Makes detection reliable and content clearly labeled

See Also