Capitulo 8: Custom Tools
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:
- “Ejecuta los tests de integración del módulo de pagos y retorna el reporte de cobertura”
- “Consulta el estado del servicio de autenticación en el ambiente especificado”
- “Genera un migration SQL para la base de datos PostgreSQL del proyecto”
Malas descripciones:
- “Ejecuta algo” (demasiado vago)
- “Herramienta útil” (no dice qué hace)
- “Tool para cosas de deploy” (impreciso)
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étodo | Tipo | Ejemplo |
|---|---|---|
tool.schema.string() | String | Nombres, rutas, texto |
tool.schema.number() | Número | Conteos, puertos, IDs |
tool.schema.boolean() | Booleano | Flags, opciones on/off |
tool.schema.enum() | Enumeración | Valores 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:
| Propiedad | Descripción |
|---|---|
context.agent | Nombre del agente que invocó la herramienta |
context.sessionID | ID de la sesión actual |
context.messageID | ID del mensaje que disparó la invocación |
context.directory | Directorio de trabajo actual |
context.worktree | Raí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:
- “Corre los tests” → ejecuta
bun test - “Corre los tests de autenticación” → ejecuta
bun test auth.test.ts - “Corre los tests con output detallado” → ejecuta
bun test --verbose
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:
migrations_list→ lista migracionesmigrations_create→ crea una nueva migración
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
| Aspecto | Custom Tools | MCP |
|---|---|---|
| Complejidad | Simple, un archivo TypeScript | Protocolo completo con servidor |
| Scope | Local al proyecto o global al usuario | Estandarizado, compartible entre herramientas |
| Configuración | Archivo en .opencode/tools/ | Requiere servidor MCP configurado |
| Lenguaje | TypeScript con plugin SDK | Cualquier lenguaje con soporte MCP |
| Uso ideal | Scripts y comandos locales | Integraciones complejas y reutilizables |
| Distribución | Copia manual o git | Paquetes 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:
- Qué hace la herramienta
- Cuándo es apropiado usarla
- Qué tipo de resultado devuelve
- Limitaciones (por ejemplo, “solo SELECT permitido”)
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
- Nunca expongas credenciales en el código de la herramienta
- Usa variables de entorno para secretos
- Limita el scope de las operaciones (solo lectura cuando sea posible)
- Valida y sanitiza todos los inputs antes de usarlos en comandos shell
Siguiente: Capitulo 9: Proveedores Cloud —>