Documentación

Ejemplos

Seis proyectos de funciones listos para desplegar — clima, issues de GitHub, notas del equipo, tiempo libre en el calendario, un resumen de noticias por correo y un escritorio de soporte que confirma los reembolsos — cada uno muestra algo que puedes copiar en tus propias herramientas.

Seis proyectos pequeños que puedes desplegar tal cual con la CLI y luego convertir en tus propias herramientas. Cada uno es un ganju.json y unos pocos archivos TypeScript cortos. Los cinco primeros agregan cada uno una capacidad del anfitrión a lo que usaba el anterior, así que leídos en orden cubren todo ctx. El sexto muestra cómo una herramienta le dice a la plataforma qué hace, para que las que no se pueden deshacer se confirmen antes de correr.

Están en la carpeta examples/ del repositorio de Ganju.

EjemploHerramientasQué te enseñaConfiguración
Climacurrent-weather, weather-forecastfetch y hosts permitidosNinguna
Issues de GitHubgithub-list-issues, github-create-issuectx.secretUn token de GitHub
Notas del equiposave-note, list-notes, read-note, find-notes, delete-notectx.resourcesNinguna
Tiempo libre en el calendariofind-free-timectx.connectionGoogle Calendar conectado
Resumen de noticiashn-top-stories, email-hn-digestctx.resources.create + ctx.sendFileGmail conectado
Escritorio de pedidosorder-lookup, order-add-note, order-refundannotations + confirmar acciones sensiblesNinguna

Antes de empezar

Necesitas Node 20 o superior y un proyecto en Pro, que es donde están disponibles las funciones.

npm install -g @ganju/cli
ganju login

git clone https://github.com/MontoyaAndres/ganju.git
cd ganju/examples/weather
ganju link       # elige la organización y el proyecto de destino

A partir de aquí todos los ejemplos funcionan igual: ganju test ejecuta una herramienta una vez sin publicarla, ganju deploy la pone en vivo y ganju logs muestra las llamadas que hace tu asistente.

ganju link completa organizationId, projectId y artifact en ganju.json. Las copias del repositorio no los traen, para que sirvan a cualquiera. Ejecuta npm install en un ejemplo si quieres que ctx se autocomplete en tu editor. El build no lo necesita.

Todos los ejemplos tienen la misma estructura:

ganju.json         las herramientas, sus esquemas y qué puede alcanzar el código
src/<tool>.ts      un archivo por herramienta, que exporta defineTool(...)
src/lib/*.ts       utilidades que comparten las herramientas

Clima

El clima actual y el pronóstico de cualquier ciudad, desde Open-Meteo. No necesita cuenta ni llave, así que es el primero que conviene desplegar.

ganju test current-weather --input '{"city":"Bogotá"}'
ganju test weather-forecast --input '{"city":"Lisbon","days":5}'
ganju deploy

Luego pregúntale a tu asistente “¿Necesito paraguas en Lisboa esta semana?”

La herramienta es un fetch normal. El único código de Ganju es defineTool y ctx.log:

// src/currentWeather.ts
export default defineTool<{ city: string }>(async (input, ctx) => {
  const place = await findPlace(input.city.trim());
  ctx.log(`${input.city} → ${place.name}`);

  const { current } = await fetchForecast(place, 1);

  return {
    place: place.name,
    ...(place.country ? { country: place.country } : {}),
    conditions: describe(current.weather_code),
    temperatureC: current.temperature_2m
    // …
  };
});

Qué observar

  • allowedHosts es lo que deja salir a fetch. Toda petición sale por la plataforma, que primero revisa el host. Una sola entrada, open-meteo.com, cubre api.open-meteo.com y geocoding-api.open-meteo.com, porque una entrada permite también sus subdominios.
  • El esquema limita la entrada antes de que corra tu código. days se declara con "minimum": 1, "maximum": 7, así que una petición de 30 días se rechaza y nunca llega al handler.
  • Omite los campos que faltan en vez de ponerlos en null. Un esquema de salida no tiene tipo nullable, así que un null en un campo string hace fallar toda la llamada. country se omite cuando el geocodificador no lo tiene.

Issues de GitHub

Lista y abre issues en tus repositorios, para que un asistente pueda revisar si un bug ya está reportado antes de crearlo.

Crea un token de acceso personal fine-grained con Issues: Read and write solo en los repositorios que quieras que alcance, y guárdalo como secreto:

read -s GANJU_SECRET_VALUE && export GANJU_SECRET_VALUE
ganju secret set GITHUB_TOKEN
unset GANJU_SECRET_VALUE

ganju test github-list-issues --input '{"repo":"your-org/your-repo","limit":5}'
ganju deploy

El token se lee al momento de la llamada y nunca se guarda con el código:

// src/lib/github.ts
const token = await ctx.secret('GITHUB_TOKEN');

const response = await fetch(`https://api.github.com${path}`, {
  headers: {
    authorization: `Bearer ${token}`,
    accept: 'application/vnd.github+json',
    'user-agent': 'ganju-example-github-issues'
  }
});

Qué observar

  • Los secretos se quedan fuera del repositorio. El token no está en el código, ni en ganju.json, ni en el bundle. Rotarlo es un ganju secret set y aplica desde la siguiente llamada, sin redesplegar.
  • El esquema puede validar más que tipos. repo tiene un pattern para owner/name, state es un enum de open, closed y all, y labels es un array de strings.
  • Los mensajes de error deben decir qué hacer. Un 401 le dice al modelo que hay que restablecer el token, y un 404 que el repositorio está mal o fuera del alcance del token. El modelo puede transmitirlo en vez de adivinar.
  • Marca lo que escribe y deja que la plataforma pregunte. github-create-issue está anotada con destructiveHint: true: publica donde otras personas lo ven. Con Confirmar acciones sensibles activado, se le pregunta a la persona antes de abrir el issue. Su descripción solo le pide al modelo que busque duplicados primero. El escritorio de pedidos muestra el patrón completo.

ganju test usa el secreto real, así que probar github-create-issue abre un issue real. Usa un repositorio de pruebas.

Notas del equipo

Una libreta en la que tu asistente puede escribir y buscar. Di “anota que el lanzamiento pasó al jueves” en Slack y pregunta “¿cuándo es el lanzamiento?” desde Claude una semana después.

ganju test save-note --input '{"title":"Release checklist","body":"1. Tag\n2. Build\n3. Deploy"}'
ganju test find-notes --input '{"query":"cómo publicamos"}'
ganju deploy

Usa los cinco métodos de ctx.resources, uno por herramienta:

HerramientaMétodo
save-notectx.resources.create({ …, index: true })
list-notesctx.resources.list()
read-notectx.resources.read(uri)
find-notesctx.resources.search(query)
delete-notectx.resources.delete(uri)
// src/saveNote.ts
const saved = await ctx.resources.create({
  title,
  uri: noteUri(title), // resource://notes/release-checklist
  content: markdown,
  mimeType: 'text/markdown',
  index: true
});

return { uri: saved.uri, created: saved.created, indexed: saved.indexed };

Qué observar

  • Un uri estable hace que guardar sea idempotente. Cada nota vive en resource://notes/<slug-del-título>, así que guardar con un título que ya existe reemplaza la nota, y created: false lo indica. El prefijo común también es lo que usan list-notes y find-notes para separar las notas de los archivos subidos y las páginas rastreadas.
  • index: true es una decisión deliberada. Lo que escribe una herramienta no es buscable a menos que lo pida. Aquí lo pide, así que las notas aparecen en find-notes y en la búsqueda de conocimiento del propio asistente. La indexación tarda unos segundos y cuenta para el almacenamiento de tu plan.
  • La búsqueda devuelve fragmentos, no documentos. Una nota puede coincidir más de una vez, así que find-notes se queda con el fragmento de mejor puntaje de cada nota.
  • resourceAccess: "own" limita lo que se puede borrar. delete-note solo puede quitar lo que escribió una herramienta, nunca un archivo que alguien subió, reciba el uri que reciba.

Este ejemplo no llama a hosts externos. Su allowedHosts está vacío, y vacío significa cualquier host público, no ninguno. Si lo extiendes para que consulte algo, agrega ese host.

Tiempo libre en el calendario

Encuentra los huecos libres de un día en tu Google Calendar donde cabe una reunión, dentro del horario laboral. Puede responder “¿Cuándo estoy libre el martes una hora?” o proponerle horarios a alguien.

Conecta Google Calendar en la página Tools del proyecto y luego:

ganju test find-free-time --input '{"date":"2026-09-21","durationMinutes":60}'
ganju deploy
{
  "date": "2026-09-21",
  "timeZone": "America/Bogota",
  "busy": [{ "start": "10:00", "end": "11:30" }, { "start": "13:00", "end": "14:15" }],
  "free": [
    { "start": "09:00", "end": "10:00", "minutes": 60 },
    { "start": "11:30", "end": "13:00", "minutes": 90 },
    { "start": "14:15", "end": "17:00", "minutes": 165 }
  ]
}

La conexión es una sola línea:

// src/lib/google.ts
const { accessToken } = await ctx.connection('google-calendar');

const response = await fetch(`https://www.googleapis.com/calendar/v3${path}`, {
  headers: { authorization: `Bearer ${accessToken}` }
});

Qué observar

  • La plataforma se encarga de OAuth. ctx.connection devuelve un access token de vida corta y lo renueva cuando hace falta. El refresh token nunca llega a tu código.
  • connections funciona como un permiso. ganju.json declara "connections": ["google-calendar"]. Sin eso la llamada se rechaza aunque la cuenta esté conectada, así que un script no puede alcanzar cuentas que no declaró.
  • Pide solo lo que la tarea necesita. La herramienta usa el endpoint free/busy de Google, que devuelve solo horarios. Ni los títulos de los eventos ni los asistentes llegan a la conversación.
  • Las zonas horarias funcionan sin librerías. src/lib/time.ts usa Intl para convertir “las 9:00 en la zona horaria del calendario” en un instante exacto, correcto también los días en que cambia la hora.

Resumen de noticias

La portada de Hacker News, en el chat o enviada por correo a alguien con la lista adjunta como archivo Markdown.

Conecta Gmail en la página Tools del proyecto y luego:

ganju test hn-top-stories --input '{"limit":5}'
ganju test email-hn-digest --input '{"to":"tu@ejemplo.com","limit":5}'
ganju deploy

La segunda prueba envía un correo real, así que envíatelo a ti.

Primero se crea el archivo, luego se envía:

// src/emailDigest.ts
const saved = await ctx.resources.create({
  title: `Hacker News digest — ${date}`,
  uri: `resource://news-digest/${date}`,
  fileName: `hacker-news-${date}.md`,
  mimeType: 'text/markdown',
  content: toMarkdown(stories, date)
});

await ctx.sendFile({
  to: 'gmail',
  uris: [saved.uri],
  message: { to: input.to, subject: `Hacker News digest — ${date}`, body }
});

Qué observar

  • sendFile recibe uris, nunca bytes. La plataforma lee el archivo del almacenamiento y lo adjunta, así que la misma llamada puede enviar un PDF de 40MB que tu código nunca podría sostener. Cambia to: 'gmail' por 'outlook' o 'slack' para enviarlo a otro lugar.
  • Enviar también requiere una conexión declarada. Enviar como una cuenta requiere el mismo permiso que tener su token, así que google-gmail está en connections.
  • Un uri por día evita que el almacenamiento se acumule. Ejecutar la herramienta dos veces en un día reemplaza el archivo de la mañana.
  • Mantén acotado el número de peticiones. Cada historia es su propia petición, hechas en paralelo. limit llega hasta 15 porque cada fetch cuenta contra el presupuesto de peticiones salientes del proyecto.

Escritorio de pedidos

El soporte de una tienda pequeña: buscar un pedido, dejarle una nota interna, reembolsarlo. No hay nada que conectar: los pedidos son datos de ejemplo en el código, y las notas y los reembolsos se guardan en el proyecto. Lo que enseña es el reembolso, que le pregunta a la persona primero sin que el código ni las descripciones lo mencionen.

ganju build --strict
ganju test order-lookup --input '{"orderId":"A-1001"}'
ganju deploy

Luego activa Confirmar acciones sensibles en Settings → Organization y pregunta “El pedido A-1001 llegó con una taza rota, ¿me reembolsas esa taza?”

Una herramienta por cada respuesta a “¿qué hace?”:

// ganju.json (resumido)
{ "name": "order-lookup",   "annotations": { "readOnlyHint": true } },
{ "name": "order-add-note", "annotations": { "readOnlyHint": false, "destructiveHint": false } },
{ "name": "order-refund",   "annotations": { "readOnlyHint": false, "destructiveHint": true } }

Con el interruptor activado, la búsqueda y la nota corren de inmediato. El reembolso no corre hasta que la persona dice que sí:

  • En un canal (Telegram, WhatsApp, Slack, Discord), el bot retiene la llamada y pregunta en el chat, mostrando debajo de su pregunta exactamente lo que un sí ejecuta:

    ⏳ Refund an order
    - orderId: A-1001
    - amount: 14
    - reason: one mug arrived chipped

    Un sí de la misma persona dentro de 30 minutos ejecuta esa llamada guardada, no una nueva que escriba el modelo. Cualquier otra respuesta la descarta.

  • En un cliente MCP (Claude, Cursor), la primera llamada vuelve como aún no ejecutada con un token de un solo uso. El asistente pregunta en su propio chat y, con un sí, vuelve a llamar con el token, que sirve una vez, para esos argumentos exactos, durante 10 minutos.

Qué notar

  • Las anotaciones son todo el mecanismo. Nada en src/orderRefund.ts comprueba una confirmación; cuando corre, la persona ya dijo que sí.
  • No pongas “confirma primero” en la descripción. La plataforma pregunta, y un modelo al que también se le pide preguntar pregunta dos veces.
  • Una herramienta sin anotaciones se confirma siempre. Según los valores por defecto de MCP, una herramienta que no dice que solo lee puede cambiar cosas, consultas incluidas. ganju build lo advierte y --strict falla.
  • ganju test no pregunta. Ejecuta la herramienta directamente, así que probar order-refund registra un reembolso real en el pedido de ejemplo.

Hazlo tuyo

La forma más rápida de escribir una herramienta nueva es copiar el ejemplo que más se le parezca:

  1. Copia la carpeta y ejecuta ganju link en ella.
  2. Renombra la herramienta en ganju.json. El router se genera a partir del manifiesto, así que el nombre solo vive ahí.
  3. Reescribe la descripción. Es lo que usa el modelo para decidir cuándo llamar la herramienta, así que di cuándo usarla, no solo qué hace.
  4. Declara las annotations. readOnlyHint: true si solo lee, destructiveHint: false si sus cambios se pueden deshacer, destructiveHint: true si envía, borra, cobra o no se puede deshacer. ganju build te dice qué falta.
  5. Declara exactamente los hosts y las conexiones que necesitas. Cualquier otro se rechaza cuando la herramienta corre.
  6. Usa ganju test hasta que pase y luego ganju deploy. Si un despliegue sale mal, ganju rollback vuelve a poner la versión anterior.

Siguiente

  • ganju CLI: todos los comandos que usan estos ejemplos, y cómo desplegar desde CI.
  • Funciones: la referencia completa de ctx, los ajustes y los límites.
  • Skill para agentes: deja que un agente de programación adapte estos ejemplos por ti.

Última actualización

Código abierto

¿Encontraste un error o tienes una idea?

Ganju es software libre. Reporta errores y pide funciones en GitHub — leemos todos los issues.