Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds
Torna alle collezioni
Guide36 min read

Agent Skills

Collection of skills and tools for extending AI agent capabilities. Complete guide to creating tool use, MCP and function calling for Claude and LLMs.

Agent Skills - Rozszerz AI Agenta

Czym są Agent Skills?

Agent Skills to wzorce, narzędzia i możliwości, które pozwalają AI agentom wykonywać specjalistyczne zadania wykraczające poza generowanie tekstu. Gdy model językowy (LLM) ma dostęp do skills/tools, staje się prawdziwym agentem - może wchodzić w interakcję ze światem zewnętrznym: scrapować strony, wykonywać zapytania do bazy danych, manipulować plikami, wysyłać emaile i integrować się z dowolnym API.

Koncepcja skills/tools jest fundamentem nowoczesnych systemów AI i występuje pod różnymi nazwami w zależności od platformy:

  • Tool Use (Anthropic Claude)
  • Function Calling (OpenAI)
  • Tools (LangChain)
  • MCP (Model Context Protocol - Anthropic)
  • Actions (GPTs)
  • Skills (Custom agents)

Ewolucja od chatbotów do agentów

Tradycyjne chatboty mogły tylko generować tekst na podstawie promptu. Nowoczesne AI agenty potrafią:

  1. Analizować - Zrozumieć zadanie użytkownika
  2. Planować - Zdecydować, które narzędzia użyć
  3. Wykonywać - Wywołać odpowiednie skills/tools
  4. Iterować - Wykorzystać wyniki do dalszych działań
  5. Raportować - Przedstawić końcowy rezultat

Ten model "ReAct" (Reasoning + Acting) pozwala agentom rozwiązywać kompleksowe, wieloetapowe problemy.

Architektura systemu ze skills

Code
TEXT
┌──────────────────────────────────────────────────────────┐
│                      User Request                         │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│                     AI Agent (LLM)                        │
│  ┌────────────────────────────────────────────────────┐  │
│  │  System Prompt + Context + Conversation History    │  │
│  └────────────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────────────┐  │
│  │  Available Tools/Skills (descriptions + schemas)   │  │
│  └────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────┘
                              ▼ (Tool Call)
┌──────────────────────────────────────────────────────────┐
│                    Skills Registry                        │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐    │
│  │Web Scraper│ │ Database │ │ FileSystem│ │   API    │    │
│  └──────────┘ └──────────┘ └──────────┘ └──────────┘    │
└──────────────────────────────────────────────────────────┘
                              ▼ (Tool Result)
┌──────────────────────────────────────────────────────────┐
│                AI Agent (continues reasoning)             │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│                     Final Response                        │
└──────────────────────────────────────────────────────────┘

Dlaczego Skills są ważne?

1. Przełamanie ograniczeń LLM

Modele językowe same w sobie:

  • Nie mają dostępu do internetu w czasie rzeczywistym
  • Nie mogą wykonywać kodu
  • Nie znają aktualnych danych (knowledge cutoff)
  • Nie mogą modyfikować systemów zewnętrznych

Skills rozwiązują te ograniczenia, dając LLM "ręce" do działania w świecie.

2. Specjalizacja i modularność

Zamiast próbować zbudować jeden "super-model" który umie wszystko, skills pozwalają:

  • Dodawać specjalistyczne możliwości modułowo
  • Aktualizować pojedyncze funkcje bez zmiany całego systemu
  • Łączyć różne skills w zależności od potrzeb
  • Testować każdy skill niezależnie

3. Bezpieczeństwo i kontrola

Skills działają jako kontrolowany interfejs między AI a światem:

  • Możesz ograniczyć, co agent może robić
  • Możesz logować i audytować wszystkie akcje
  • Możesz dodać rate limiting i sandboxing
  • Możesz wymagać approval dla niebezpiecznych operacji

4. Redukcja halucynacji

Gdy LLM ma dostęp do rzeczywistych danych przez skills:

  • Nie musi "zgadywać" informacji
  • Może zweryfikować fakty przed odpowiedzią
  • Odpowiedzi są oparte na aktualnych danych
  • Mniejsze ryzyko confabulation

Anatomy of a Skill

Podstawowa struktura skill

Code
TypeScript
interface Skill {
  // Identyfikator używany przez LLM do wywołania
  name: string

  // Opis dla LLM - co robi ten skill
  description: string

  // Schema parametrów (JSON Schema lub Zod)
  inputSchema: JSONSchema | ZodSchema

  // Opcjonalny schema outputu
  outputSchema?: JSONSchema | ZodSchema

  // Główna logika wykonania
  execute: (params: unknown) => Promise<unknown>

  // Opcjonalne metadane
  metadata?: {
    category?: string
    requiresAuth?: boolean
    rateLimit?: number
    timeout?: number
  }
}

Przykład kompletnego skill

Code
TypeScript
import { z } from 'zod'

// Schema parametrów z Zod
const weatherInputSchema = z.object({
  city: z.string().describe('Nazwa miasta'),
  units: z.enum(['metric', 'imperial']).default('metric').describe('Jednostki temperatury'),
  lang: z.string().default('pl').describe('Język odpowiedzi')
})

// Schema outputu
const weatherOutputSchema = z.object({
  temperature: z.number(),
  description: z.string(),
  humidity: z.number(),
  wind_speed: z.number()
})

// Skill definition
export const weatherSkill: Skill = {
  name: 'get_weather',
  description: `Pobiera aktualną pogodę dla podanego miasta.
    Użyj tego narzędzia gdy użytkownik pyta o pogodę, temperaturę,
    czy będzie padać, jakie warunki atmosferyczne.`,

  inputSchema: weatherInputSchema,
  outputSchema: weatherOutputSchema,

  metadata: {
    category: 'information',
    requiresAuth: true,  // Wymaga API key
    rateLimit: 60,       // Max 60 requests/minute
    timeout: 5000        // 5 second timeout
  },

  async execute(params) {
    const { city, units, lang } = weatherInputSchema.parse(params)

    const apiKey = process.env.OPENWEATHER_API_KEY
    const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&units=${units}&lang=${lang}&appid=${apiKey}`

    const response = await fetch(url)

    if (!response.ok) {
      throw new Error(`Weather API error: ${response.status}`)
    }

    const data = await response.json()

    return weatherOutputSchema.parse({
      temperature: data.main.temp,
      description: data.weather[0].description,
      humidity: data.main.humidity,
      wind_speed: data.wind.speed
    })
  }
}

Popularne kategorie skills

1. Web & Internet Skills

Code
TypeScript
// Web Scraper - pobieranie treści ze stron
export const webScraperSkill = {
  name: 'web_scrape',
  description: 'Pobiera i parsuje treść ze strony internetowej',

  inputSchema: z.object({
    url: z.string().url(),
    selector: z.string().optional().describe('CSS selector do wybranych elementów'),
    format: z.enum(['text', 'html', 'markdown']).default('text')
  }),

  async execute({ url, selector, format }) {
    // Fetch z headless browser lub prostym fetch
    const response = await fetch(url, {
      headers: {
        'User-Agent': 'Mozilla/5.0 (compatible; Bot/1.0)'
      }
    })

    const html = await response.text()

    // Parse HTML
    const dom = new JSDOM(html)
    const document = dom.window.document

    let content: string

    if (selector) {
      const elements = document.querySelectorAll(selector)
      content = Array.from(elements).map(el => el.textContent).join('\n')
    } else {
      // Wyciągnij główną treść
      const article = document.querySelector('article, main, .content')
      content = article?.textContent || document.body.textContent || ''
    }

    // Format output
    if (format === 'markdown') {
      return turndownService.turndown(content)
    }

    return content.trim()
  }
}

// Search - wyszukiwanie w internecie
export const searchSkill = {
  name: 'web_search',
  description: 'Wyszukuje informacje w internecie używając DuckDuckGo/Serper',

  inputSchema: z.object({
    query: z.string(),
    num_results: z.number().min(1).max(10).default(5)
  }),

  async execute({ query, num_results }) {
    // Użyj Serper, SerpAPI, lub DuckDuckGo API
    const response = await fetch('https://google.serper.dev/search', {
      method: 'POST',
      headers: {
        'X-API-KEY': process.env.SERPER_API_KEY!,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ q: query, num: num_results })
    })

    const data = await response.json()

    return data.organic.map((result: any) => ({
      title: result.title,
      link: result.link,
      snippet: result.snippet
    }))
  }
}

2. Database Skills

Code
TypeScript
// Safe SQL Query
export const databaseQuerySkill = {
  name: 'database_query',
  description: `Wykonuje bezpieczne zapytanie SQL do bazy danych.
    TYLKO SELECT queries są dozwolone. Użyj parametrów dla wartości.`,

  inputSchema: z.object({
    query: z.string().describe('SQL SELECT query'),
    params: z.array(z.any()).default([]).describe('Parametry do query')
  }),

  async execute({ query, params }) {
    // Walidacja - tylko SELECT
    const normalizedQuery = query.trim().toLowerCase()
    if (!normalizedQuery.startsWith('select')) {
      throw new Error('Only SELECT queries are allowed')
    }

    // Blacklist niebezpiecznych słów kluczowych
    const forbidden = ['drop', 'delete', 'update', 'insert', 'alter', 'create', 'truncate']
    for (const word of forbidden) {
      if (normalizedQuery.includes(word)) {
        throw new Error(`Forbidden keyword: ${word}`)
      }
    }

    // Wykonaj query z parametrami (prevents SQL injection)
    const result = await db.query(query, params)

    // Ogranicz ilość wyników
    return result.rows.slice(0, 100)
  }
}

// Prisma Query (type-safe)
export const prismaSkill = {
  name: 'prisma_query',
  description: 'Wykonuje type-safe query przez Prisma ORM',

  inputSchema: z.object({
    model: z.enum(['User', 'Post', 'Comment', 'Product']),
    operation: z.enum(['findMany', 'findFirst', 'count']),
    where: z.record(z.any()).optional(),
    select: z.array(z.string()).optional(),
    take: z.number().max(100).optional(),
    skip: z.number().optional(),
    orderBy: z.record(z.enum(['asc', 'desc'])).optional()
  }),

  async execute({ model, operation, where, select, take, skip, orderBy }) {
    const prismaModel = prisma[model.toLowerCase() as keyof typeof prisma]

    const query: any = {
      where,
      take: take || 50,
      skip
    }

    if (select) {
      query.select = Object.fromEntries(select.map(s => [s, true]))
    }

    if (orderBy) {
      query.orderBy = orderBy
    }

    // @ts-ignore - dynamic model access
    return await prismaModel[operation](query)
  }
}

3. File System Skills

Code
TypeScript
// Bezpieczne operacje na plikach
export const fileSystemSkill = {
  name: 'filesystem',
  description: 'Operacje na systemie plików w sandboxowanym katalogu',

  inputSchema: z.object({
    action: z.enum(['read', 'write', 'list', 'exists', 'mkdir', 'delete']),
    path: z.string(),
    content: z.string().optional()
  }),

  metadata: {
    // Sandbox do określonego katalogu
    sandboxPath: '/workspace',
    maxFileSize: 10 * 1024 * 1024 // 10MB
  },

  async execute({ action, path, content }) {
    // Walidacja ścieżki - nie pozwalaj na wyjście z sandbox
    const sandboxPath = '/workspace'
    const fullPath = join(sandboxPath, path)
    const normalizedPath = normalize(fullPath)

    if (!normalizedPath.startsWith(sandboxPath)) {
      throw new Error('Path traversal attack detected')
    }

    switch (action) {
      case 'read':
        const fileContent = await fs.readFile(normalizedPath, 'utf-8')
        return { content: fileContent.slice(0, 100000) } // Limit size

      case 'write':
        if (!content) throw new Error('Content required for write')
        if (content.length > 10 * 1024 * 1024) throw new Error('File too large')
        await fs.writeFile(normalizedPath, content)
        return { success: true, path: normalizedPath }

      case 'list':
        const entries = await fs.readdir(normalizedPath, { withFileTypes: true })
        return entries.map(e => ({
          name: e.name,
          type: e.isDirectory() ? 'directory' : 'file'
        }))

      case 'exists':
        try {
          await fs.access(normalizedPath)
          return { exists: true }
        } catch {
          return { exists: false }
        }

      case 'mkdir':
        await fs.mkdir(normalizedPath, { recursive: true })
        return { success: true }

      case 'delete':
        await fs.unlink(normalizedPath)
        return { success: true }
    }
  }
}

4. Code Execution Skills

Code
TypeScript
// JavaScript/TypeScript execution (sandboxed)
export const codeExecutionSkill = {
  name: 'execute_code',
  description: 'Wykonuje kod JavaScript w bezpiecznym sandbox',

  inputSchema: z.object({
    code: z.string(),
    language: z.enum(['javascript', 'typescript']).default('javascript'),
    timeout: z.number().max(30000).default(5000)
  }),

  async execute({ code, language, timeout }) {
    // Użyj VM2 lub isolated-vm dla bezpieczeństwa
    const vm = new NodeVM({
      timeout,
      sandbox: {
        console: {
          log: (...args: any[]) => logs.push(args.join(' ')),
          error: (...args: any[]) => errors.push(args.join(' '))
        }
      },
      require: {
        external: false, // Nie pozwalaj na require
        builtin: ['util', 'path'] // Tylko bezpieczne moduły
      }
    })

    const logs: string[] = []
    const errors: string[] = []

    try {
      // Dla TypeScript - transpiluj najpierw
      let executableCode = code
      if (language === 'typescript') {
        const result = ts.transpileModule(code, {
          compilerOptions: { module: ts.ModuleKind.CommonJS }
        })
        executableCode = result.outputText
      }

      const result = vm.run(executableCode)

      return {
        success: true,
        result,
        logs,
        errors
      }
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error',
        logs,
        errors
      }
    }
  }
}

// Python execution (via subprocess)
export const pythonSkill = {
  name: 'execute_python',
  description: 'Wykonuje kod Python',

  inputSchema: z.object({
    code: z.string(),
    timeout: z.number().max(60000).default(10000)
  }),

  async execute({ code, timeout }) {
    return new Promise((resolve, reject) => {
      const python = spawn('python3', ['-c', code], {
        timeout,
        env: {
          ...process.env,
          PYTHONDONTWRITEBYTECODE: '1'
        }
      })

      let stdout = ''
      let stderr = ''

      python.stdout.on('data', (data) => { stdout += data })
      python.stderr.on('data', (data) => { stderr += data })

      python.on('close', (exitCode) => {
        resolve({
          success: exitCode === 0,
          stdout: stdout.trim(),
          stderr: stderr.trim(),
          exitCode
        })
      })

      python.on('error', (err) => {
        reject(new Error(`Python execution failed: ${err.message}`))
      })
    })
  }
}

5. API Integration Skills

Code
TypeScript
// Generic API caller
export const apiCallerSkill = {
  name: 'api_call',
  description: 'Wykonuje HTTP request do zewnętrznego API',

  inputSchema: z.object({
    url: z.string().url(),
    method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).default('GET'),
    headers: z.record(z.string()).optional(),
    body: z.any().optional(),
    timeout: z.number().max(30000).default(10000)
  }),

  async execute({ url, method, headers, body, timeout }) {
    // Whitelist dozwolonych domen (security)
    const allowedDomains = [
      'api.openai.com',
      'api.anthropic.com',
      'api.github.com',
      'api.stripe.com'
    ]

    const urlObj = new URL(url)
    if (!allowedDomains.some(d => urlObj.hostname.endsWith(d))) {
      throw new Error(`Domain not allowed: ${urlObj.hostname}`)
    }

    const controller = new AbortController()
    const timeoutId = setTimeout(() => controller.abort(), timeout)

    try {
      const response = await fetch(url, {
        method,
        headers: {
          'Content-Type': 'application/json',
          ...headers
        },
        body: body ? JSON.stringify(body) : undefined,
        signal: controller.signal
      })

      const contentType = response.headers.get('content-type')
      const data = contentType?.includes('application/json')
        ? await response.json()
        : await response.text()

      return {
        status: response.status,
        statusText: response.statusText,
        data
      }
    } finally {
      clearTimeout(timeoutId)
    }
  }
}

// GitHub API skill
export const githubSkill = {
  name: 'github',
  description: 'Interakcja z GitHub API - repos, issues, PRs',

  inputSchema: z.object({
    action: z.enum([
      'get_repo',
      'list_issues',
      'create_issue',
      'get_pr',
      'list_prs',
      'get_file'
    ]),
    owner: z.string(),
    repo: z.string(),
    params: z.record(z.any()).optional()
  }),

  async execute({ action, owner, repo, params }) {
    const octokit = new Octokit({
      auth: process.env.GITHUB_TOKEN
    })

    switch (action) {
      case 'get_repo':
        return await octokit.repos.get({ owner, repo })

      case 'list_issues':
        return await octokit.issues.listForRepo({
          owner,
          repo,
          state: params?.state || 'open',
          per_page: params?.limit || 10
        })

      case 'create_issue':
        return await octokit.issues.create({
          owner,
          repo,
          title: params?.title,
          body: params?.body,
          labels: params?.labels
        })

      case 'list_prs':
        return await octokit.pulls.list({
          owner,
          repo,
          state: params?.state || 'open',
          per_page: params?.limit || 10
        })

      case 'get_file':
        const content = await octokit.repos.getContent({
          owner,
          repo,
          path: params?.path
        })
        // Decode base64 content
        if ('content' in content.data) {
          return {
            ...content.data,
            decoded: Buffer.from(content.data.content, 'base64').toString()
          }
        }
        return content.data
    }
  }
}

6. Communication Skills

Code
TypeScript
// Email sending skill
export const emailSkill = {
  name: 'send_email',
  description: 'Wysyła email przez Resend/SendGrid',

  inputSchema: z.object({
    to: z.string().email(),
    subject: z.string().max(200),
    body: z.string().max(10000),
    html: z.boolean().default(false)
  }),

  metadata: {
    requiresApproval: true, // Wymaga potwierdzenia użytkownika
    rateLimit: 10 // Max 10 emails/hour
  },

  async execute({ to, subject, body, html }) {
    const resend = new Resend(process.env.RESEND_API_KEY)

    const result = await resend.emails.send({
      from: 'assistant@example.com',
      to,
      subject,
      [html ? 'html' : 'text']: body
    })

    return { success: true, id: result.id }
  }
}

// Slack messaging skill
export const slackSkill = {
  name: 'slack_message',
  description: 'Wysyła wiadomość na Slack',

  inputSchema: z.object({
    channel: z.string(),
    message: z.string(),
    thread_ts: z.string().optional()
  }),

  async execute({ channel, message, thread_ts }) {
    const slack = new WebClient(process.env.SLACK_BOT_TOKEN)

    const result = await slack.chat.postMessage({
      channel,
      text: message,
      thread_ts
    })

    return {
      success: true,
      ts: result.ts,
      channel: result.channel
    }
  }
}

Skills Registry

Implementacja registry

TSskills/registry.ts
TypeScript
// skills/registry.ts
import { Skill } from './types'

class SkillsRegistry {
  private skills: Map<string, Skill> = new Map()
  private categories: Map<string, Skill[]> = new Map()

  register(skill: Skill) {
    // Walidacja skill
    this.validateSkill(skill)

    // Rejestracja
    this.skills.set(skill.name, skill)

    // Kategoryzacja
    const category = skill.metadata?.category || 'general'
    if (!this.categories.has(category)) {
      this.categories.set(category, [])
    }
    this.categories.get(category)!.push(skill)

    console.log(`Registered skill: ${skill.name}`)
  }

  private validateSkill(skill: Skill) {
    if (!skill.name || typeof skill.name !== 'string') {
      throw new Error('Skill must have a name')
    }
    if (!skill.description || typeof skill.description !== 'string') {
      throw new Error('Skill must have a description')
    }
    if (typeof skill.execute !== 'function') {
      throw new Error('Skill must have an execute function')
    }
    if (this.skills.has(skill.name)) {
      throw new Error(`Skill ${skill.name} already registered`)
    }
  }

  get(name: string): Skill | undefined {
    return this.skills.get(name)
  }

  getAll(): Skill[] {
    return Array.from(this.skills.values())
  }

  getByCategory(category: string): Skill[] {
    return this.categories.get(category) || []
  }

  // Format dla LLM (tool definitions)
  toToolDefinitions() {
    return this.getAll().map(skill => ({
      name: skill.name,
      description: skill.description,
      input_schema: this.zodToJsonSchema(skill.inputSchema)
    }))
  }

  private zodToJsonSchema(schema: z.ZodSchema) {
    // Konwertuj Zod schema do JSON Schema
    return zodToJsonSchema(schema)
  }

  async execute(name: string, params: unknown) {
    const skill = this.get(name)
    if (!skill) {
      throw new Error(`Skill not found: ${name}`)
    }

    // Rate limiting
    if (skill.metadata?.rateLimit) {
      await this.checkRateLimit(name, skill.metadata.rateLimit)
    }

    // Timeout
    const timeout = skill.metadata?.timeout || 30000
    const timeoutPromise = new Promise((_, reject) => {
      setTimeout(() => reject(new Error(`Skill ${name} timed out`)), timeout)
    })

    // Execute with timeout
    try {
      const result = await Promise.race([
        skill.execute(params),
        timeoutPromise
      ])

      // Log execution
      this.logExecution(name, params, result, true)

      return result
    } catch (error) {
      this.logExecution(name, params, error, false)
      throw error
    }
  }

  private async checkRateLimit(skillName: string, limit: number) {
    // Implementacja rate limiting (np. z Redis)
    const key = `ratelimit:${skillName}`
    const count = await redis.incr(key)

    if (count === 1) {
      await redis.expire(key, 60) // 1 minute window
    }

    if (count > limit) {
      throw new Error(`Rate limit exceeded for skill: ${skillName}`)
    }
  }

  private logExecution(name: string, params: unknown, result: unknown, success: boolean) {
    console.log(JSON.stringify({
      timestamp: new Date().toISOString(),
      skill: name,
      params: this.sanitizeForLog(params),
      success,
      result: success ? this.sanitizeForLog(result) : result
    }))
  }

  private sanitizeForLog(data: unknown): unknown {
    // Usuń wrażliwe dane przed logowaniem
    if (typeof data === 'object' && data !== null) {
      const sanitized = { ...data as object }
      const sensitiveKeys = ['password', 'token', 'apiKey', 'secret']
      for (const key of sensitiveKeys) {
        if (key in sanitized) {
          (sanitized as any)[key] = '[REDACTED]'
        }
      }
      return sanitized
    }
    return data
  }
}

// Singleton instance
export const skillsRegistry = new SkillsRegistry()

// Rejestracja skills
import { webScraperSkill } from './skills/web-scraper'
import { databaseQuerySkill } from './skills/database'
import { fileSystemSkill } from './skills/filesystem'
import { weatherSkill } from './skills/weather'
import { githubSkill } from './skills/github'

skillsRegistry.register(webScraperSkill)
skillsRegistry.register(databaseQuerySkill)
skillsRegistry.register(fileSystemSkill)
skillsRegistry.register(weatherSkill)
skillsRegistry.register(githubSkill)

Integracja z Claude

Tool Use API

Code
TypeScript
import Anthropic from '@anthropic-ai/sdk'
import { skillsRegistry } from './skills/registry'

const anthropic = new Anthropic()

async function runAgentWithTools(userMessage: string) {
  // Pobierz definicje narzędzi
  const tools = skillsRegistry.toToolDefinitions()

  // Pierwszy request do Claude
  let response = await anthropic.messages.create({
    model: 'claude-sonnet-4-5-20241022',
    max_tokens: 4096,
    system: `Jesteś pomocnym asystentem z dostępem do narzędzi.
             Używaj narzędzi gdy potrzebujesz aktualnych informacji
             lub wykonać akcję. Odpowiadaj po polsku.`,
    tools,
    messages: [{ role: 'user', content: userMessage }]
  })

  // Pętla obsługi tool use
  while (response.stop_reason === 'tool_use') {
    const toolUseBlocks = response.content.filter(
      block => block.type === 'tool_use'
    )

    // Wykonaj wszystkie tool calls
    const toolResults = await Promise.all(
      toolUseBlocks.map(async (block) => {
        if (block.type !== 'tool_use') return null

        try {
          const result = await skillsRegistry.execute(block.name, block.input)
          return {
            type: 'tool_result' as const,
            tool_use_id: block.id,
            content: JSON.stringify(result)
          }
        } catch (error) {
          return {
            type: 'tool_result' as const,
            tool_use_id: block.id,
            content: JSON.stringify({
              error: error instanceof Error ? error.message : 'Unknown error'
            }),
            is_error: true
          }
        }
      })
    )

    // Kontynuuj konwersację z wynikami
    response = await anthropic.messages.create({
      model: 'claude-sonnet-4-5-20241022',
      max_tokens: 4096,
      system: `Jesteś pomocnym asystentem z dostępem do narzędzi.`,
      tools,
      messages: [
        { role: 'user', content: userMessage },
        { role: 'assistant', content: response.content },
        { role: 'user', content: toolResults.filter(Boolean) as any }
      ]
    })
  }

  // Zwróć finalną odpowiedź
  const textBlock = response.content.find(block => block.type === 'text')
  return textBlock?.type === 'text' ? textBlock.text : ''
}

// Użycie
const answer = await runAgentWithTools(
  'Jaka jest pogoda w Warszawie i ile mam otwartych issues na repo example/test?'
)

MCP (Model Context Protocol)

MCP to nowszy standard od Anthropic dla integracji narzędzi:

TSmcp-server/index.ts
TypeScript
// mcp-server/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'

const server = new Server(
  { name: 'my-skills-server', version: '1.0.0' },
  { capabilities: { tools: {} } }
)

// Rejestracja tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: 'get_weather',
        description: 'Pobiera aktualną pogodę dla miasta',
        inputSchema: {
          type: 'object',
          properties: {
            city: { type: 'string', description: 'Nazwa miasta' }
          },
          required: ['city']
        }
      }
    ]
  }
})

// Handler dla tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params

  switch (name) {
    case 'get_weather':
      const weather = await getWeather(args.city)
      return {
        content: [{ type: 'text', text: JSON.stringify(weather) }]
      }

    default:
      throw new Error(`Unknown tool: ${name}`)
  }
})

// Start server
const transport = new StdioServerTransport()
await server.connect(transport)

Best Practices

1. Security First

Code
TypeScript
// Zawsze waliduj input
async execute(params) {
  // Zod schema validation
  const validated = inputSchema.parse(params)

  // Sanitize user input
  validated.query = sanitizeHtml(validated.query)

  // Check permissions
  if (!userHasPermission('skill:execute')) {
    throw new Error('Permission denied')
  }

  // ...
}

2. Error Handling

Code
TypeScript
async execute(params) {
  try {
    const result = await someOperation()
    return { success: true, data: result }
  } catch (error) {
    // Log full error internally
    console.error('Skill error:', error)

    // Return sanitized error to LLM
    return {
      success: false,
      error: error instanceof Error
        ? error.message
        : 'An error occurred'
    }
  }
}

3. Idempotency

Code
TypeScript
// Dla skills które modyfikują stan, używaj idempotency keys
async execute({ action, data, idempotencyKey }) {
  // Sprawdź czy już wykonano
  const existing = await cache.get(`idempotent:${idempotencyKey}`)
  if (existing) {
    return existing // Zwróć cached result
  }

  // Wykonaj akcję
  const result = await performAction(data)

  // Cache result
  await cache.set(`idempotent:${idempotencyKey}`, result, 3600)

  return result
}

4. Logging & Monitoring

Code
TypeScript
// Structured logging dla każdego skill execution
const executeWithLogging = async (skill: Skill, params: unknown) => {
  const startTime = Date.now()
  const requestId = crypto.randomUUID()

  console.log(JSON.stringify({
    event: 'skill_start',
    requestId,
    skill: skill.name,
    params: sanitize(params),
    timestamp: new Date().toISOString()
  }))

  try {
    const result = await skill.execute(params)

    console.log(JSON.stringify({
      event: 'skill_success',
      requestId,
      skill: skill.name,
      duration: Date.now() - startTime,
      resultSize: JSON.stringify(result).length
    }))

    return result
  } catch (error) {
    console.log(JSON.stringify({
      event: 'skill_error',
      requestId,
      skill: skill.name,
      duration: Date.now() - startTime,
      error: error instanceof Error ? error.message : 'Unknown'
    }))

    throw error
  }
}

5. Rate Limiting & Throttling

Code
TypeScript
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '1m'), // 10 requests per minute
})

async function executeWithRateLimit(skillName: string, userId: string) {
  const { success, limit, remaining, reset } = await ratelimit.limit(
    `${skillName}:${userId}`
  )

  if (!success) {
    throw new Error(`Rate limit exceeded. Try again in ${reset}ms`)
  }

  // Execute skill...
}

Testowanie Skills

Code
TypeScript
import { describe, it, expect, vi } from 'vitest'
import { weatherSkill } from './skills/weather'

describe('weatherSkill', () => {
  it('should return weather data for valid city', async () => {
    // Mock fetch
    vi.spyOn(global, 'fetch').mockResolvedValueOnce({
      ok: true,
      json: async () => ({
        main: { temp: 20, humidity: 65 },
        weather: [{ description: 'sunny' }],
        wind: { speed: 5 }
      })
    } as Response)

    const result = await weatherSkill.execute({ city: 'Warsaw' })

    expect(result).toEqual({
      temperature: 20,
      description: 'sunny',
      humidity: 65,
      wind_speed: 5
    })
  })

  it('should throw error for invalid city', async () => {
    vi.spyOn(global, 'fetch').mockResolvedValueOnce({
      ok: false,
      status: 404
    } as Response)

    await expect(
      weatherSkill.execute({ city: 'NonExistentCity12345' })
    ).rejects.toThrow('Weather API error: 404')
  })

  it('should validate input schema', async () => {
    await expect(
      weatherSkill.execute({ city: 123 }) // number instead of string
    ).rejects.toThrow()
  })
})

Cennik

KomponentKoszt
Skill developmentCzas developera
Claude API (tool use)Standardowe API rates
MCP Server hostingZależy od infrastruktury
Third-party APIsZależy od dostawcy

Skills są kodem - nie ma dodatkowych opłat za ich używanie. Koszty to:

  • API calls do LLM (Claude, OpenAI)
  • Hosting infrastruktury (serwery, bazy danych)
  • Third-party services (weather API, GitHub, etc.)

FAQ - Najczęściej zadawane pytania

Ile skills może mieć agent?

Teoretycznie nieograniczenie, ale praktycznie:

  • Claude wspiera do 64 tools w jednym request
  • Więcej tools = więcej tokenów na opisy
  • LLM lepiej radzi sobie z mniejszą liczbą dobrze opisanych skills

Jak wybrać między Tool Use a MCP?

  • Tool Use API: Prostsze, direct integration, mniej setup
  • MCP: Standard protokół, lepsze tooling, reusable servers

Dla prostych przypadków użyj Tool Use. Dla złożonych systemów z wieloma integracjami rozważ MCP.

Czy skills mogą wywoływać inne skills?

Tak, ale z ostrożnością. Lepsze podejście:

  1. LLM decyduje o sekwencji skills
  2. Lub użyj "orchestrator" skill który koordynuje inne

Jak obsłużyć długie operacje?

Code
TypeScript
// Użyj async patterns
async execute(params) {
  // Start job
  const jobId = await startLongJob(params)

  // Return job ID, let LLM check status later
  return {
    status: 'started',
    jobId,
    checkStatusWith: 'check_job_status'
  }
}

Jak zabezpieczyć wrażliwe skills?

  1. Approval workflow - Niektóre skills wymagają potwierdzenia użytkownika
  2. Scoped permissions - Różni użytkownicy mają dostęp do różnych skills
  3. Audit logging - Loguj wszystkie wywołania
  4. Rate limiting - Ogranicz częstotliwość

Czy mogę używać skills z różnymi LLM?

Tak, ale format może się różnić:

  • Claude: Tool Use API / MCP
  • OpenAI: Function Calling
  • LangChain: Tools (abstrakcja nad różnymi modelami)

Warto zbudować abstrakcję która wspiera wiele providers.


Agent Skills - extend your AI agent

What are Agent Skills?

Agent Skills are patterns, tools, and capabilities that allow AI agents to perform specialized tasks beyond text generation. When a language model (LLM) has access to skills/tools, it becomes a true agent - it can interact with the external world: scrape websites, execute database queries, manipulate files, send emails, and integrate with any API.

The concept of skills/tools is the foundation of modern AI systems and appears under different names depending on the platform:

  • Tool Use (Anthropic Claude)
  • Function Calling (OpenAI)
  • Tools (LangChain)
  • MCP (Model Context Protocol - Anthropic)
  • Actions (GPTs)
  • Skills (Custom agents)

Evolution from chatbots to agents

Traditional chatbots could only generate text based on a prompt. Modern AI agents can:

  1. Analyze - Understand the user's task
  2. Plan - Decide which tools to use
  3. Execute - Call the appropriate skills/tools
  4. Iterate - Use results for further actions
  5. Report - Present the final result

This "ReAct" (Reasoning + Acting) model allows agents to solve complex, multi-step problems.

System architecture with skills

Code
TEXT
┌──────────────────────────────────────────────────────────┐
│                      User Request                         │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│                     AI Agent (LLM)                        │
│  ┌────────────────────────────────────────────────────┐  │
│  │  System Prompt + Context + Conversation History    │  │
│  └────────────────────────────────────────────────────┘  │
│  ┌────────────────────────────────────────────────────┐  │
│  │  Available Tools/Skills (descriptions + schemas)   │  │
│  └────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────┘
                              ▼ (Tool Call)
┌──────────────────────────────────────────────────────────┐
│                    Skills Registry                        │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐    │
│  │Web Scraper│ │ Database │ │ FileSystem│ │   API    │    │
│  └──────────┘ └──────────┘ └──────────┘ └──────────┘    │
└──────────────────────────────────────────────────────────┘
                              ▼ (Tool Result)
┌──────────────────────────────────────────────────────────┐
│                AI Agent (continues reasoning)             │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│                     Final Response                        │
└──────────────────────────────────────────────────────────┘

Why are Skills important?

1. Breaking LLM limitations

Language models on their own:

  • Have no access to the internet in real time
  • Cannot execute code
  • Do not know current data (knowledge cutoff)
  • Cannot modify external systems

Skills solve these limitations by giving the LLM "hands" to act in the world.

2. Specialization and modularity

Instead of trying to build a single "super-model" that can do everything, skills allow you to:

  • Add specialized capabilities in a modular way
  • Update individual functions without changing the entire system
  • Combine different skills depending on needs
  • Test each skill independently

3. Security and control

Skills act as a controlled interface between AI and the world:

  • You can limit what the agent can do
  • You can log and audit all actions
  • You can add rate limiting and sandboxing
  • You can require approval for dangerous operations

4. Reducing hallucinations

When an LLM has access to real data through skills:

  • It does not have to "guess" information
  • It can verify facts before responding
  • Responses are based on current data
  • Lower risk of confabulation

Anatomy of a Skill

Basic skill structure

Code
TypeScript
interface Skill {
  // Identifier used by LLM for invocation
  name: string

  // Description for LLM - what this skill does
  description: string

  // Parameter schema (JSON Schema or Zod)
  inputSchema: JSONSchema | ZodSchema

  // Optional output schema
  outputSchema?: JSONSchema | ZodSchema

  // Main execution logic
  execute: (params: unknown) => Promise<unknown>

  // Optional metadata
  metadata?: {
    category?: string
    requiresAuth?: boolean
    rateLimit?: number
    timeout?: number
  }
}

Complete skill example

Code
TypeScript
import { z } from 'zod'

// Parameter schema with Zod
const weatherInputSchema = z.object({
  city: z.string().describe('City name'),
  units: z.enum(['metric', 'imperial']).default('metric').describe('Temperature units'),
  lang: z.string().default('en').describe('Response language')
})

// Output schema
const weatherOutputSchema = z.object({
  temperature: z.number(),
  description: z.string(),
  humidity: z.number(),
  wind_speed: z.number()
})

// Skill definition
export const weatherSkill: Skill = {
  name: 'get_weather',
  description: `Fetches the current weather for a given city.
    Use this tool when the user asks about the weather, temperature,
    whether it will rain, or what atmospheric conditions are like.`,

  inputSchema: weatherInputSchema,
  outputSchema: weatherOutputSchema,

  metadata: {
    category: 'information',
    requiresAuth: true,  // Requires API key
    rateLimit: 60,       // Max 60 requests/minute
    timeout: 5000        // 5 second timeout
  },

  async execute(params) {
    const { city, units, lang } = weatherInputSchema.parse(params)

    const apiKey = process.env.OPENWEATHER_API_KEY
    const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&units=${units}&lang=${lang}&appid=${apiKey}`

    const response = await fetch(url)

    if (!response.ok) {
      throw new Error(`Weather API error: ${response.status}`)
    }

    const data = await response.json()

    return weatherOutputSchema.parse({
      temperature: data.main.temp,
      description: data.weather[0].description,
      humidity: data.main.humidity,
      wind_speed: data.wind.speed
    })
  }
}

Popular skill categories

1. Web & Internet Skills

Code
TypeScript
export const webScraperSkill = {
  name: 'web_scrape',
  description: 'Fetches and parses content from a web page',

  inputSchema: z.object({
    url: z.string().url(),
    selector: z.string().optional().describe('CSS selector for specific elements'),
    format: z.enum(['text', 'html', 'markdown']).default('text')
  }),

  async execute({ url, selector, format }) {
    const response = await fetch(url, {
      headers: {
        'User-Agent': 'Mozilla/5.0 (compatible; Bot/1.0)'
      }
    })

    const html = await response.text()

    const dom = new JSDOM(html)
    const document = dom.window.document

    let content: string

    if (selector) {
      const elements = document.querySelectorAll(selector)
      content = Array.from(elements).map(el => el.textContent).join('\n')
    } else {
      const article = document.querySelector('article, main, .content')
      content = article?.textContent || document.body.textContent || ''
    }

    if (format === 'markdown') {
      return turndownService.turndown(content)
    }

    return content.trim()
  }
}

export const searchSkill = {
  name: 'web_search',
  description: 'Searches the internet using DuckDuckGo/Serper',

  inputSchema: z.object({
    query: z.string(),
    num_results: z.number().min(1).max(10).default(5)
  }),

  async execute({ query, num_results }) {
    const response = await fetch('https://google.serper.dev/search', {
      method: 'POST',
      headers: {
        'X-API-KEY': process.env.SERPER_API_KEY!,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ q: query, num: num_results })
    })

    const data = await response.json()

    return data.organic.map((result: any) => ({
      title: result.title,
      link: result.link,
      snippet: result.snippet
    }))
  }
}

2. Database Skills

Code
TypeScript
export const databaseQuerySkill = {
  name: 'database_query',
  description: `Executes a safe SQL query against the database.
    ONLY SELECT queries are allowed. Use parameters for values.`,

  inputSchema: z.object({
    query: z.string().describe('SQL SELECT query'),
    params: z.array(z.any()).default([]).describe('Query parameters')
  }),

  async execute({ query, params }) {
    const normalizedQuery = query.trim().toLowerCase()
    if (!normalizedQuery.startsWith('select')) {
      throw new Error('Only SELECT queries are allowed')
    }

    const forbidden = ['drop', 'delete', 'update', 'insert', 'alter', 'create', 'truncate']
    for (const word of forbidden) {
      if (normalizedQuery.includes(word)) {
        throw new Error(`Forbidden keyword: ${word}`)
      }
    }

    const result = await db.query(query, params)

    return result.rows.slice(0, 100)
  }
}

export const prismaSkill = {
  name: 'prisma_query',
  description: 'Executes a type-safe query through Prisma ORM',

  inputSchema: z.object({
    model: z.enum(['User', 'Post', 'Comment', 'Product']),
    operation: z.enum(['findMany', 'findFirst', 'count']),
    where: z.record(z.any()).optional(),
    select: z.array(z.string()).optional(),
    take: z.number().max(100).optional(),
    skip: z.number().optional(),
    orderBy: z.record(z.enum(['asc', 'desc'])).optional()
  }),

  async execute({ model, operation, where, select, take, skip, orderBy }) {
    const prismaModel = prisma[model.toLowerCase() as keyof typeof prisma]

    const query: any = {
      where,
      take: take || 50,
      skip
    }

    if (select) {
      query.select = Object.fromEntries(select.map(s => [s, true]))
    }

    if (orderBy) {
      query.orderBy = orderBy
    }

    // @ts-ignore - dynamic model access
    return await prismaModel[operation](query)
  }
}

3. File System Skills

Code
TypeScript
export const fileSystemSkill = {
  name: 'filesystem',
  description: 'File system operations in a sandboxed directory',

  inputSchema: z.object({
    action: z.enum(['read', 'write', 'list', 'exists', 'mkdir', 'delete']),
    path: z.string(),
    content: z.string().optional()
  }),

  metadata: {
    sandboxPath: '/workspace',
    maxFileSize: 10 * 1024 * 1024
  },

  async execute({ action, path, content }) {
    const sandboxPath = '/workspace'
    const fullPath = join(sandboxPath, path)
    const normalizedPath = normalize(fullPath)

    if (!normalizedPath.startsWith(sandboxPath)) {
      throw new Error('Path traversal attack detected')
    }

    switch (action) {
      case 'read':
        const fileContent = await fs.readFile(normalizedPath, 'utf-8')
        return { content: fileContent.slice(0, 100000) }

      case 'write':
        if (!content) throw new Error('Content required for write')
        if (content.length > 10 * 1024 * 1024) throw new Error('File too large')
        await fs.writeFile(normalizedPath, content)
        return { success: true, path: normalizedPath }

      case 'list':
        const entries = await fs.readdir(normalizedPath, { withFileTypes: true })
        return entries.map(e => ({
          name: e.name,
          type: e.isDirectory() ? 'directory' : 'file'
        }))

      case 'exists':
        try {
          await fs.access(normalizedPath)
          return { exists: true }
        } catch {
          return { exists: false }
        }

      case 'mkdir':
        await fs.mkdir(normalizedPath, { recursive: true })
        return { success: true }

      case 'delete':
        await fs.unlink(normalizedPath)
        return { success: true }
    }
  }
}

4. Code Execution Skills

Code
TypeScript
export const codeExecutionSkill = {
  name: 'execute_code',
  description: 'Executes JavaScript code in a secure sandbox',

  inputSchema: z.object({
    code: z.string(),
    language: z.enum(['javascript', 'typescript']).default('javascript'),
    timeout: z.number().max(30000).default(5000)
  }),

  async execute({ code, language, timeout }) {
    const vm = new NodeVM({
      timeout,
      sandbox: {
        console: {
          log: (...args: any[]) => logs.push(args.join(' ')),
          error: (...args: any[]) => errors.push(args.join(' '))
        }
      },
      require: {
        external: false,
        builtin: ['util', 'path']
      }
    })

    const logs: string[] = []
    const errors: string[] = []

    try {
      let executableCode = code
      if (language === 'typescript') {
        const result = ts.transpileModule(code, {
          compilerOptions: { module: ts.ModuleKind.CommonJS }
        })
        executableCode = result.outputText
      }

      const result = vm.run(executableCode)

      return {
        success: true,
        result,
        logs,
        errors
      }
    } catch (error) {
      return {
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error',
        logs,
        errors
      }
    }
  }
}

export const pythonSkill = {
  name: 'execute_python',
  description: 'Executes Python code',

  inputSchema: z.object({
    code: z.string(),
    timeout: z.number().max(60000).default(10000)
  }),

  async execute({ code, timeout }) {
    return new Promise((resolve, reject) => {
      const python = spawn('python3', ['-c', code], {
        timeout,
        env: {
          ...process.env,
          PYTHONDONTWRITEBYTECODE: '1'
        }
      })

      let stdout = ''
      let stderr = ''

      python.stdout.on('data', (data) => { stdout += data })
      python.stderr.on('data', (data) => { stderr += data })

      python.on('close', (exitCode) => {
        resolve({
          success: exitCode === 0,
          stdout: stdout.trim(),
          stderr: stderr.trim(),
          exitCode
        })
      })

      python.on('error', (err) => {
        reject(new Error(`Python execution failed: ${err.message}`))
      })
    })
  }
}

5. API Integration Skills

Code
TypeScript
export const apiCallerSkill = {
  name: 'api_call',
  description: 'Executes an HTTP request to an external API',

  inputSchema: z.object({
    url: z.string().url(),
    method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']).default('GET'),
    headers: z.record(z.string()).optional(),
    body: z.any().optional(),
    timeout: z.number().max(30000).default(10000)
  }),

  async execute({ url, method, headers, body, timeout }) {
    const allowedDomains = [
      'api.openai.com',
      'api.anthropic.com',
      'api.github.com',
      'api.stripe.com'
    ]

    const urlObj = new URL(url)
    if (!allowedDomains.some(d => urlObj.hostname.endsWith(d))) {
      throw new Error(`Domain not allowed: ${urlObj.hostname}`)
    }

    const controller = new AbortController()
    const timeoutId = setTimeout(() => controller.abort(), timeout)

    try {
      const response = await fetch(url, {
        method,
        headers: {
          'Content-Type': 'application/json',
          ...headers
        },
        body: body ? JSON.stringify(body) : undefined,
        signal: controller.signal
      })

      const contentType = response.headers.get('content-type')
      const data = contentType?.includes('application/json')
        ? await response.json()
        : await response.text()

      return {
        status: response.status,
        statusText: response.statusText,
        data
      }
    } finally {
      clearTimeout(timeoutId)
    }
  }
}

export const githubSkill = {
  name: 'github',
  description: 'Interaction with GitHub API - repos, issues, PRs',

  inputSchema: z.object({
    action: z.enum([
      'get_repo',
      'list_issues',
      'create_issue',
      'get_pr',
      'list_prs',
      'get_file'
    ]),
    owner: z.string(),
    repo: z.string(),
    params: z.record(z.any()).optional()
  }),

  async execute({ action, owner, repo, params }) {
    const octokit = new Octokit({
      auth: process.env.GITHUB_TOKEN
    })

    switch (action) {
      case 'get_repo':
        return await octokit.repos.get({ owner, repo })

      case 'list_issues':
        return await octokit.issues.listForRepo({
          owner,
          repo,
          state: params?.state || 'open',
          per_page: params?.limit || 10
        })

      case 'create_issue':
        return await octokit.issues.create({
          owner,
          repo,
          title: params?.title,
          body: params?.body,
          labels: params?.labels
        })

      case 'list_prs':
        return await octokit.pulls.list({
          owner,
          repo,
          state: params?.state || 'open',
          per_page: params?.limit || 10
        })

      case 'get_file':
        const content = await octokit.repos.getContent({
          owner,
          repo,
          path: params?.path
        })
        if ('content' in content.data) {
          return {
            ...content.data,
            decoded: Buffer.from(content.data.content, 'base64').toString()
          }
        }
        return content.data
    }
  }
}

6. Communication Skills

Code
TypeScript
export const emailSkill = {
  name: 'send_email',
  description: 'Sends an email via Resend/SendGrid',

  inputSchema: z.object({
    to: z.string().email(),
    subject: z.string().max(200),
    body: z.string().max(10000),
    html: z.boolean().default(false)
  }),

  metadata: {
    requiresApproval: true,
    rateLimit: 10
  },

  async execute({ to, subject, body, html }) {
    const resend = new Resend(process.env.RESEND_API_KEY)

    const result = await resend.emails.send({
      from: 'assistant@example.com',
      to,
      subject,
      [html ? 'html' : 'text']: body
    })

    return { success: true, id: result.id }
  }
}

export const slackSkill = {
  name: 'slack_message',
  description: 'Sends a message on Slack',

  inputSchema: z.object({
    channel: z.string(),
    message: z.string(),
    thread_ts: z.string().optional()
  }),

  async execute({ channel, message, thread_ts }) {
    const slack = new WebClient(process.env.SLACK_BOT_TOKEN)

    const result = await slack.chat.postMessage({
      channel,
      text: message,
      thread_ts
    })

    return {
      success: true,
      ts: result.ts,
      channel: result.channel
    }
  }
}

Skills Registry

Registry implementation

Code
TypeScript
import { Skill } from './types'

class SkillsRegistry {
  private skills: Map<string, Skill> = new Map()
  private categories: Map<string, Skill[]> = new Map()

  register(skill: Skill) {
    this.validateSkill(skill)

    this.skills.set(skill.name, skill)

    const category = skill.metadata?.category || 'general'
    if (!this.categories.has(category)) {
      this.categories.set(category, [])
    }
    this.categories.get(category)!.push(skill)

    console.log(`Registered skill: ${skill.name}`)
  }

  private validateSkill(skill: Skill) {
    if (!skill.name || typeof skill.name !== 'string') {
      throw new Error('Skill must have a name')
    }
    if (!skill.description || typeof skill.description !== 'string') {
      throw new Error('Skill must have a description')
    }
    if (typeof skill.execute !== 'function') {
      throw new Error('Skill must have an execute function')
    }
    if (this.skills.has(skill.name)) {
      throw new Error(`Skill ${skill.name} already registered`)
    }
  }

  get(name: string): Skill | undefined {
    return this.skills.get(name)
  }

  getAll(): Skill[] {
    return Array.from(this.skills.values())
  }

  getByCategory(category: string): Skill[] {
    return this.categories.get(category) || []
  }

  toToolDefinitions() {
    return this.getAll().map(skill => ({
      name: skill.name,
      description: skill.description,
      input_schema: this.zodToJsonSchema(skill.inputSchema)
    }))
  }

  private zodToJsonSchema(schema: z.ZodSchema) {
    return zodToJsonSchema(schema)
  }

  async execute(name: string, params: unknown) {
    const skill = this.get(name)
    if (!skill) {
      throw new Error(`Skill not found: ${name}`)
    }

    if (skill.metadata?.rateLimit) {
      await this.checkRateLimit(name, skill.metadata.rateLimit)
    }

    const timeout = skill.metadata?.timeout || 30000
    const timeoutPromise = new Promise((_, reject) => {
      setTimeout(() => reject(new Error(`Skill ${name} timed out`)), timeout)
    })

    try {
      const result = await Promise.race([
        skill.execute(params),
        timeoutPromise
      ])

      this.logExecution(name, params, result, true)

      return result
    } catch (error) {
      this.logExecution(name, params, error, false)
      throw error
    }
  }

  private async checkRateLimit(skillName: string, limit: number) {
    const key = `ratelimit:${skillName}`
    const count = await redis.incr(key)

    if (count === 1) {
      await redis.expire(key, 60)
    }

    if (count > limit) {
      throw new Error(`Rate limit exceeded for skill: ${skillName}`)
    }
  }

  private logExecution(name: string, params: unknown, result: unknown, success: boolean) {
    console.log(JSON.stringify({
      timestamp: new Date().toISOString(),
      skill: name,
      params: this.sanitizeForLog(params),
      success,
      result: success ? this.sanitizeForLog(result) : result
    }))
  }

  private sanitizeForLog(data: unknown): unknown {
    if (typeof data === 'object' && data !== null) {
      const sanitized = { ...data as object }
      const sensitiveKeys = ['password', 'token', 'apiKey', 'secret']
      for (const key of sensitiveKeys) {
        if (key in sanitized) {
          (sanitized as any)[key] = '[REDACTED]'
        }
      }
      return sanitized
    }
    return data
  }
}

export const skillsRegistry = new SkillsRegistry()

import { webScraperSkill } from './skills/web-scraper'
import { databaseQuerySkill } from './skills/database'
import { fileSystemSkill } from './skills/filesystem'
import { weatherSkill } from './skills/weather'
import { githubSkill } from './skills/github'

skillsRegistry.register(webScraperSkill)
skillsRegistry.register(databaseQuerySkill)
skillsRegistry.register(fileSystemSkill)
skillsRegistry.register(weatherSkill)
skillsRegistry.register(githubSkill)

Integration with Claude

Tool Use API

Code
TypeScript
import Anthropic from '@anthropic-ai/sdk'
import { skillsRegistry } from './skills/registry'

const anthropic = new Anthropic()

async function runAgentWithTools(userMessage: string) {
  const tools = skillsRegistry.toToolDefinitions()

  let response = await anthropic.messages.create({
    model: 'claude-sonnet-4-5-20241022',
    max_tokens: 4096,
    system: `You are a helpful assistant with access to tools.
             Use tools when you need current information
             or to perform an action. Respond in English.`,
    tools,
    messages: [{ role: 'user', content: userMessage }]
  })

  while (response.stop_reason === 'tool_use') {
    const toolUseBlocks = response.content.filter(
      block => block.type === 'tool_use'
    )

    const toolResults = await Promise.all(
      toolUseBlocks.map(async (block) => {
        if (block.type !== 'tool_use') return null

        try {
          const result = await skillsRegistry.execute(block.name, block.input)
          return {
            type: 'tool_result' as const,
            tool_use_id: block.id,
            content: JSON.stringify(result)
          }
        } catch (error) {
          return {
            type: 'tool_result' as const,
            tool_use_id: block.id,
            content: JSON.stringify({
              error: error instanceof Error ? error.message : 'Unknown error'
            }),
            is_error: true
          }
        }
      })
    )

    response = await anthropic.messages.create({
      model: 'claude-sonnet-4-5-20241022',
      max_tokens: 4096,
      system: `You are a helpful assistant with access to tools.`,
      tools,
      messages: [
        { role: 'user', content: userMessage },
        { role: 'assistant', content: response.content },
        { role: 'user', content: toolResults.filter(Boolean) as any }
      ]
    })
  }

  const textBlock = response.content.find(block => block.type === 'text')
  return textBlock?.type === 'text' ? textBlock.text : ''
}

const answer = await runAgentWithTools(
  'What is the weather in Warsaw and how many open issues do I have on the example/test repo?'
)

MCP (Model Context Protocol)

MCP is a newer standard from Anthropic for tool integration:

Code
TypeScript
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'

const server = new Server(
  { name: 'my-skills-server', version: '1.0.0' },
  { capabilities: { tools: {} } }
)

server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: 'get_weather',
        description: 'Fetches the current weather for a city',
        inputSchema: {
          type: 'object',
          properties: {
            city: { type: 'string', description: 'City name' }
          },
          required: ['city']
        }
      }
    ]
  }
})

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params

  switch (name) {
    case 'get_weather':
      const weather = await getWeather(args.city)
      return {
        content: [{ type: 'text', text: JSON.stringify(weather) }]
      }

    default:
      throw new Error(`Unknown tool: ${name}`)
  }
})

const transport = new StdioServerTransport()
await server.connect(transport)

Best practices

1. Security first

Code
TypeScript
async execute(params) {
  const validated = inputSchema.parse(params)

  validated.query = sanitizeHtml(validated.query)

  if (!userHasPermission('skill:execute')) {
    throw new Error('Permission denied')
  }

  // ...
}

2. Error handling

Code
TypeScript
async execute(params) {
  try {
    const result = await someOperation()
    return { success: true, data: result }
  } catch (error) {
    console.error('Skill error:', error)

    return {
      success: false,
      error: error instanceof Error
        ? error.message
        : 'An error occurred'
    }
  }
}

3. Idempotency

Code
TypeScript
async execute({ action, data, idempotencyKey }) {
  const existing = await cache.get(`idempotent:${idempotencyKey}`)
  if (existing) {
    return existing
  }

  const result = await performAction(data)

  await cache.set(`idempotent:${idempotencyKey}`, result, 3600)

  return result
}

4. Logging & monitoring

Code
TypeScript
const executeWithLogging = async (skill: Skill, params: unknown) => {
  const startTime = Date.now()
  const requestId = crypto.randomUUID()

  console.log(JSON.stringify({
    event: 'skill_start',
    requestId,
    skill: skill.name,
    params: sanitize(params),
    timestamp: new Date().toISOString()
  }))

  try {
    const result = await skill.execute(params)

    console.log(JSON.stringify({
      event: 'skill_success',
      requestId,
      skill: skill.name,
      duration: Date.now() - startTime,
      resultSize: JSON.stringify(result).length
    }))

    return result
  } catch (error) {
    console.log(JSON.stringify({
      event: 'skill_error',
      requestId,
      skill: skill.name,
      duration: Date.now() - startTime,
      error: error instanceof Error ? error.message : 'Unknown'
    }))

    throw error
  }
}

5. Rate limiting & throttling

Code
TypeScript
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '1m'),
})

async function executeWithRateLimit(skillName: string, userId: string) {
  const { success, limit, remaining, reset } = await ratelimit.limit(
    `${skillName}:${userId}`
  )

  if (!success) {
    throw new Error(`Rate limit exceeded. Try again in ${reset}ms`)
  }

  // Execute skill...
}

Testing skills

Code
TypeScript
import { describe, it, expect, vi } from 'vitest'
import { weatherSkill } from './skills/weather'

describe('weatherSkill', () => {
  it('should return weather data for valid city', async () => {
    vi.spyOn(global, 'fetch').mockResolvedValueOnce({
      ok: true,
      json: async () => ({
        main: { temp: 20, humidity: 65 },
        weather: [{ description: 'sunny' }],
        wind: { speed: 5 }
      })
    } as Response)

    const result = await weatherSkill.execute({ city: 'Warsaw' })

    expect(result).toEqual({
      temperature: 20,
      description: 'sunny',
      humidity: 65,
      wind_speed: 5
    })
  })

  it('should throw error for invalid city', async () => {
    vi.spyOn(global, 'fetch').mockResolvedValueOnce({
      ok: false,
      status: 404
    } as Response)

    await expect(
      weatherSkill.execute({ city: 'NonExistentCity12345' })
    ).rejects.toThrow('Weather API error: 404')
  })

  it('should validate input schema', async () => {
    await expect(
      weatherSkill.execute({ city: 123 })
    ).rejects.toThrow()
  })
})

Pricing

ComponentCost
Skill developmentDeveloper time
Claude API (tool use)Standard API rates
MCP Server hostingDepends on infrastructure
Third-party APIsDepends on provider

Skills are code - there are no additional fees for using them. The costs are:

  • API calls to LLM (Claude, OpenAI)
  • Infrastructure hosting (servers, databases)
  • Third-party services (weather API, GitHub, etc.)

FAQ - frequently asked questions

How many skills can an agent have?

Theoretically unlimited, but in practice:

  • Claude supports up to 64 tools in a single request
  • More tools = more tokens for descriptions
  • LLMs perform better with fewer well-described skills

How to choose between Tool Use and MCP?

  • Tool Use API: Simpler, direct integration, less setup
  • MCP: Standard protocol, better tooling, reusable servers

For simple use cases, use Tool Use. For complex systems with many integrations, consider MCP.

Can skills call other skills?

Yes, but with caution. A better approach:

  1. The LLM decides on the sequence of skills
  2. Or use an "orchestrator" skill that coordinates others

How to handle long-running operations?

Code
TypeScript
async execute(params) {
  const jobId = await startLongJob(params)

  return {
    status: 'started',
    jobId,
    checkStatusWith: 'check_job_status'
  }
}

How to secure sensitive skills?

  1. Approval workflow - Some skills require user confirmation
  2. Scoped permissions - Different users have access to different skills
  3. Audit logging - Log all invocations
  4. Rate limiting - Limit frequency

Can I use skills with different LLMs?

Yes, but the format may differ:

  • Claude: Tool Use API / MCP
  • OpenAI: Function Calling
  • LangChain: Tools (abstraction over different models)

It is worth building an abstraction that supports multiple providers.