Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds
Powrót do kolekcji
Przewodnik36 min czytania

Agent Skills

Kolekcja skills i narzędzi do rozszerzania możliwości AI agentów. Kompletny przewodnik po tworzeniu tool use, MCP i function calling dla Claude i LLM.

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.