Capitulo 8: Custom Tools

Por: Artiko
opencodeaitoolscustomtypescriptplugin

Capitulo 8: Custom Tools

< Volver al Indice del Tutorial

Las herramientas built-in cubren operaciones generales de desarrollo, pero cada proyecto tiene necesidades únicas que van más allá de leer archivos y ejecutar comandos. Las custom tools te permiten crear funciones escritas en TypeScript que el LLM puede llamar durante las conversaciones, extendiendo sus capacidades con lógica específica de tu proyecto.

A diferencia de las herramientas built-in que son genéricas, las custom tools pueden interactuar con APIs internas, ejecutar scripts especializados, conectar con bases de datos, automatizar deploys y cualquier operación que tu proyecto necesite.

Qué son las Custom Tools

Una custom tool es una función definida usando el plugin SDK de OpenCode (@opencode-ai/plugin). Cuando el LLM determina que necesita ejecutar esa función, la invoca con los parámetros apropiados y recibe el resultado. Es como darle al LLM acceso a funciones personalizadas que tú diseñas.

Las custom tools trabajan junto a las herramientas built-in. El LLM elige cuál usar según el contexto de la conversación y la descripción de cada herramienta. Por eso es fundamental que las descripciones sean claras y precisas: son lo que el LLM lee para decidir cuándo invocar cada tool.

Ubicación de Custom Tools

Las custom tools se definen como archivos TypeScript en directorios específicos:

Por Proyecto

.opencode/tools/mi-herramienta.ts

Solo disponible en ese proyecto. Ideal para herramientas que son específicas del codebase y que no tienen sentido fuera del contexto del proyecto.

Global (todos los proyectos)

~/.config/opencode/tools/mi-herramienta.ts

Disponible en cualquier proyecto donde uses OpenCode. Ideal para herramientas genéricas que usas en múltiples proyectos, como formateo de logs, consultas a servicios compartidos o utilidades de desarrollo comunes.

flowchart TD
    A[Custom Tools] --> B[Por proyecto]
    A --> C[Global]
    B --> D[".opencode/tools/*.ts"]
    C --> E["~/.config/opencode/tools/*.ts"]
    D --> F[Solo este proyecto]
    E --> G[Todos los proyectos]
    style B fill:#4CAF50,color:#fff
    style C fill:#2196F3,color:#fff

Anatomía de una Custom Tool

Cada custom tool se define usando la función tool del plugin SDK. La estructura básica es:

import { tool } from "@opencode-ai/plugin"

export default tool({
  description: "Tu descripción aquí",
  args: {
    parametro: tool.schema.string().describe("Descripción del parámetro"),
  },
  async execute(args, context) {
    // Lógica de la herramienta
    return resultado
  },
})

Vamos a desglosar cada parte:

Import del SDK

import { tool } from "@opencode-ai/plugin"

El módulo @opencode-ai/plugin proporciona la función tool que registra tu herramienta en OpenCode. Este import es obligatorio y es el punto de entrada del sistema de plugins.

Descripción

description: "Tu descripción aquí"

La descripción es crítica. Es lo que el LLM lee para decidir cuándo usar tu herramienta. Debe ser clara, específica y describir exactamente qué hace la herramienta y en qué situaciones es útil. Una descripción vaga resulta en que el LLM no la use o la use incorrectamente.

Buenas descripciones:

Malas descripciones:

Esquema de Argumentos

args: {
  parametro: tool.schema.string().describe("Descripción del parámetro"),
  numero: tool.schema.number().describe("Un valor numérico"),
  opcional: tool.schema.string().optional().describe("Parámetro opcional"),
}

El esquema usa tool.schema que proporciona métodos para definir tipos:

MétodoTipoEjemplo
tool.schema.string()StringNombres, rutas, texto
tool.schema.number()NúmeroConteos, puertos, IDs
tool.schema.boolean()BooleanoFlags, opciones on/off
tool.schema.enum()EnumeraciónValores predefinidos

Cada parámetro puede tener .describe() para explicar al LLM qué debe pasar y .optional() para marcarlo como no requerido.

Función Execute

async execute(args, context) {
  // args: los parámetros que pasó el LLM
  // context: información del entorno
  return resultado
}

La función execute recibe dos parámetros:

args: Los argumentos que el LLM pasó según el esquema definido. Están tipados automáticamente según la definición de args.

context: Información del entorno de ejecución:

PropiedadDescripción
context.agentNombre del agente que invocó la herramienta
context.sessionIDID de la sesión actual
context.messageIDID del mensaje que disparó la invocación
context.directoryDirectorio de trabajo actual
context.worktreeRaíz del worktree del proyecto

El valor retornado se presenta al LLM como el resultado de la herramienta. Puede ser un string, un objeto o cualquier dato serializable.

Ejemplos Prácticos

Herramienta de Tests Específicos

Una herramienta que ejecuta tests de un archivo o patrón específico:

import { tool } from "@opencode-ai/plugin"

export default tool({
  description: "Ejecuta tests unitarios. Puede correr todos los tests o filtrar por archivo o patron",
  args: {
    file: tool.schema.string().optional().describe("Archivo o patron de test"),
    verbose: tool.schema.boolean().optional().describe("Mostrar output detallado"),
  },
  async execute(args, context) {
    const cmd = ["bun", "test"]
    if (args.file) cmd.push(args.file)
    if (args.verbose) cmd.push("--verbose")
    const result = await Bun.$`${cmd}`.text()
    return result.trim()
  },
})

Con esta herramienta, puedes decirle al LLM:

El LLM mapea tu instrucción en lenguaje natural a los parámetros correctos de la herramienta.

Herramienta que Invoca Python

Las custom tools no están limitadas a TypeScript. Puedes invocar scripts en cualquier lenguaje:

import { tool } from "@opencode-ai/plugin"
import path from "path"

export default tool({
  description: "Suma dos numeros usando un script Python",
  args: {
    a: tool.schema.number().describe("Primer numero"),
    b: tool.schema.number().describe("Segundo numero"),
  },
  async execute(args, context) {
    const script = path.join(context.worktree, ".opencode/tools/add.py")
    const result = await Bun.$`python3 ${script} ${args.a} ${args.b}`.text()
    return result.trim()
  },
})

Nota cómo usa context.worktree para construir la ruta al script Python relativa a la raíz del proyecto. Esto asegura que la herramienta funcione independientemente del directorio de trabajo actual.

Este patrón es poderoso porque te permite integrar cualquier herramienta de línea de comandos: scripts Python para machine learning, herramientas Go para procesamiento de datos, comandos Rust para operaciones de alto rendimiento, etc.

Herramienta de Estado de Servicio

Consultar el estado de un servicio interno:

import { tool } from "@opencode-ai/plugin"

export default tool({
  description: "Consulta el health check de un microservicio del proyecto",
  args: {
    service: tool.schema.string().describe("Nombre del servicio: auth, payments, users, notifications"),
    environment: tool.schema.string().optional().describe("Ambiente: dev, staging, production. Default: dev"),
  },
  async execute(args, context) {
    const env = args.environment || "dev"
    const baseUrl = env === "production"
      ? "https://api.miproyecto.com"
      : `https://${env}.api.miproyecto.com`
    const url = `${baseUrl}/${args.service}/health`
    const response = await fetch(url)
    const data = await response.json()
    return JSON.stringify(data, null, 2)
  },
})

Herramienta de Base de Datos

Ejecutar queries de solo lectura contra la base de datos de desarrollo:

import { tool } from "@opencode-ai/plugin"

export default tool({
  description: "Ejecuta una query SQL de solo lectura contra la base de datos de desarrollo. Solo SELECT permitido",
  args: {
    query: tool.schema.string().describe("Query SQL. Solo SELECT es permitido"),
  },
  async execute(args, context) {
    if (!args.query.trim().toUpperCase().startsWith("SELECT")) {
      return "Error: Solo queries SELECT son permitidas"
    }
    const result = await Bun.$`psql $DATABASE_URL -c ${args.query} --csv`.text()
    return result.trim()
  },
})

La validación de que solo se permiten queries SELECT es una buena práctica de seguridad. La herramienta rechaza cualquier intento de INSERT, UPDATE, DELETE u otros comandos destructivos.

Convenciones de Nombres

El sistema de nombres de custom tools sigue reglas simples pero importantes:

Nombre del archivo = Nombre de la herramienta

El nombre del archivo TypeScript (sin extensión) se convierte en el nombre de la herramienta:

.opencode/tools/run-tests.ts    → herramienta "run-tests"
.opencode/tools/health-check.ts → herramienta "health-check"
.opencode/tools/db-query.ts     → herramienta "db-query"

Múltiples herramientas por archivo

Un archivo puede exportar múltiples herramientas usando exports nombrados. El nombre de la herramienta se forma como archivo_exportacion:

import { tool } from "@opencode-ai/plugin"

export const list = tool({
  description: "Lista todas las migraciones de la base de datos",
  args: {},
  async execute() {
    const result = await Bun.$`bun run migrations:list`.text()
    return result.trim()
  },
})

export const create = tool({
  description: "Crea una nueva migracion de base de datos",
  args: {
    name: tool.schema.string().describe("Nombre descriptivo de la migracion"),
  },
  async execute(args) {
    const result = await Bun.$`bun run migrations:create ${args.name}`.text()
    return result.trim()
  },
})

Si el archivo se llama migrations.ts, las herramientas se nombran:

Este patrón es excelente para agrupar herramientas relacionadas en un solo archivo, manteniendo la organización sin dispersar lógica en múltiples archivos pequeños.

Reemplazar herramientas built-in

Las custom tools pueden reemplazar herramientas built-in si usan el mismo nombre. Esto te permite personalizar el comportamiento de herramientas existentes:

// .opencode/tools/bash.ts - Reemplaza la herramienta bash built-in
import { tool } from "@opencode-ai/plugin"

export default tool({
  description: "Ejecuta comandos shell con logging adicional",
  args: {
    command: tool.schema.string().describe("Comando a ejecutar"),
  },
  async execute(args, context) {
    console.log(`[${new Date().toISOString()}] Ejecutando: ${args.command}`)
    const result = await Bun.$`${args.command}`.text()
    console.log(`[${new Date().toISOString()}] Completado`)
    return result.trim()
  },
})

Usa esta capacidad con precaución. Reemplazar una built-in significa que el comportamiento original ya no está disponible.

Custom Tools vs MCP

Es importante entender cuándo usar custom tools y cuándo optar por MCP (Model Context Protocol):

flowchart TD
    A{Necesidad} --> B{Es local al proyecto?}
    B -->|Si| C{Es un script/comando simple?}
    C -->|Si| D[Custom Tool]
    C -->|No| E{Lo usaran otros proyectos?}
    E -->|No| D
    E -->|Si| F[MCP Server]
    B -->|No| G{Es una integracion estandar?}
    G -->|Si| F
    G -->|No| D
AspectoCustom ToolsMCP
ComplejidadSimple, un archivo TypeScriptProtocolo completo con servidor
ScopeLocal al proyecto o global al usuarioEstandarizado, compartible entre herramientas
ConfiguraciónArchivo en .opencode/tools/Requiere servidor MCP configurado
LenguajeTypeScript con plugin SDKCualquier lenguaje con soporte MCP
Uso idealScripts y comandos localesIntegraciones complejas y reutilizables
DistribuciónCopia manual o gitPaquetes npm, registros MCP

Custom tools son la opción rápida y simple para necesidades locales. Las defines en minutos y están listas para usar. MCP es el protocolo estandarizado para integraciones más complejas que necesitan ser compartidas entre proyectos, equipos o herramientas de IA diferentes.

La regla general: si necesitas ejecutar un script o comando del proyecto, usa custom tools. Si necesitas una integración robusta con un servicio externo que múltiples proyectos o herramientas podrían usar, considera MCP.

Buenas Prácticas

Descripciones Precisas

El LLM decide cuándo usar la herramienta basándose exclusivamente en la descripción. Sé específico sobre:

Validación de Inputs

No dependas solo del LLM para pasar parámetros correctos. Valida en tu función execute:

async execute(args, context) {
  if (!args.query.trim().toUpperCase().startsWith("SELECT")) {
    return "Error: Solo queries SELECT son permitidas"
  }
  // ...
}

Mensajes de Error Claros

Cuando algo falla, retorna mensajes que el LLM pueda interpretar y comunicar al usuario:

async execute(args, context) {
  try {
    const result = await fetchData(args.url)
    return result
  } catch (error) {
    return `Error al consultar ${args.url}: ${error.message}. Verifica que el servicio esté activo.`
  }
}

Idempotencia

Diseña tools que puedan ejecutarse múltiples veces sin efectos inesperados. Si la herramienta crea un recurso, debería verificar si ya existe antes de crearlo de nuevo.

Output Estructurado

Retorna datos en formatos que el LLM pueda interpretar fácilmente. JSON es ideal para datos estructurados, texto plano para resultados simples:

return JSON.stringify({
  status: "success",
  tests_passed: 42,
  tests_failed: 0,
  coverage: "87.3%"
}, null, 2)

Usa context.worktree para Rutas

Siempre usa context.worktree para construir rutas relativas al proyecto en vez de rutas absolutas hardcodeadas:

const configPath = path.join(context.worktree, "config/database.yml")

Esto asegura que la herramienta funcione correctamente incluso cuando el directorio de trabajo cambia.

Seguridad


Siguiente: Capitulo 9: Proveedores Cloud —>