From c4ef30066f3007fd4a81d4cd35200682e69b3e62 Mon Sep 17 00:00:00 2001 From: Mark Henderson Date: Mon, 27 Apr 2026 17:55:57 -0700 Subject: [PATCH] Expand chat panel to full MCP tool surface (35+ tools) vibn-tools.ts previously exposed only 12 of the 35+ MCP tools. Now includes the complete surface from AI_CAPABILITIES.md: - workspace.describe, gitea.credentials - apps: get, update, rewire_git, delete, deploy, deployments, exec, volumes.list/wipe, containers.up/ps, repair, domains.list/set, envs.list/upsert/delete - databases: list, create, get, update, delete - auth: list, create, delete - domains: search, get, attach (+ existing register, list) - storage: describe, provision, inject_env Action dispatch simplified: toolName.replace(/_/g, '.') maps any tool name to the MCP action with no explicit lookup table needed. System prompt updated to reflect full capability set. Made-with: Cursor --- app/api/chat/route.ts | 28 +- lib/ai/vibn-tools.ts | 626 ++++++++++++++++++++++++++++++++---------- 2 files changed, 499 insertions(+), 155 deletions(-) diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 34e017ad..a7964632 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -73,20 +73,22 @@ When a user asks what's live, call \`apps_list\` — not \`projects_get\`. Proje ${projectsText} ## Your capabilities -You can take real actions by calling tools: -- List/inspect projects and deployed apps (\`apps_list\` shows live services) -- Deploy new applications from templates (n8n, WordPress, Twenty CRM, etc.) or Docker images -- Register and manage custom domains -- View application logs -- Search available app templates -- Search GitHub for open-source projects to reference or fork -- Fetch any public URL or file +You have full access to the Vibn platform. You can: +- **Apps**: list, create, update, delete, deploy, get logs, exec commands, manage domains, manage env vars, manage volumes, repair broken deploys +- **Databases**: provision Postgres/MySQL/Redis/MongoDB and 5 other flavors, get connection URLs +- **Auth providers**: deploy Pocketbase, Authentik, Keycloak, Logto, SuperTokens, and others +- **Domains**: search availability, register, attach to apps with full DNS wiring +- **Storage**: provision GCS buckets, inject S3-compatible credentials into apps +- **Templates**: search and deploy from 320+ one-click app templates (n8n, Ghost, Supabase, etc.) +- **GitHub**: search open-source repos, read any file from a public repo +- **Web**: fetch any public URL or documentation page -## Guidelines -- Be concise and action-oriented. If the user wants to deploy something, do it — don't just describe how. -- When deploying, always confirm the app name and domain with the user first unless they've been explicit. -- After a tool call, summarize what happened in plain language. -- If a deployment takes time, let the user know it may take a few minutes. +## Key rules +- Call \`apps_list\` (not \`projects_list\`) when the user asks what is running or what has a domain. +- Always call \`apps_templates_search\` before \`apps_create\` for any popular third-party app. +- Be concise and action-oriented — if the user wants to deploy something, do it, don't just describe how. +- Confirm app name and domain before deploying unless the user has been explicit. +- After tool calls, summarize in plain language what happened. - Format code, URLs, and technical values in backticks. - Today's date: ${new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })}.`; } diff --git a/lib/ai/vibn-tools.ts b/lib/ai/vibn-tools.ts index 7d23cf89..5a4d534c 100644 --- a/lib/ai/vibn-tools.ts +++ b/lib/ai/vibn-tools.ts @@ -1,118 +1,522 @@ /** - * Vibn MCP tool definitions for the chat assistant. - * These are surfaced to Gemini as function declarations so the model - * can take actions (deploy apps, list projects, etc.) on behalf of the user. + * Vibn MCP tool definitions for the Gemini chat assistant. + * + * These mirror the full MCP surface documented in AI_CAPABILITIES.md. + * Tool names use underscores (e.g. apps_create) which map 1:1 to the + * MCP action names (e.g. apps.create) by replacing _ with . + * + * Non-MCP tools (github_search, github_file, http_fetch) are handled + * locally at the bottom of this file. */ import type { ToolDefinition } from './gemini-chat'; const GITHUB_TOKEN = process.env.GITHUB_TOKEN || ''; export const VIBN_TOOL_DEFINITIONS: ToolDefinition[] = [ + + // ── Workspace & identity ───────────────────────────────────────────────── + + { + name: 'workspace_describe', + description: 'Returns workspace details: slug, Coolify project UUID, Gitea org, and provision status.', + parameters: { type: 'OBJECT', properties: {}, required: [] }, + }, + { + name: 'gitea_credentials', + description: 'Returns the workspace bot\'s Gitea username, PAT, clone URL template, and SSH remote template. Use for any git clone/push operations.', + parameters: { type: 'OBJECT', properties: {}, required: [] }, + }, + + // ── Projects ───────────────────────────────────────────────────────────── + { name: 'projects_list', - description: 'List all projects in the user\'s workspace with their status, last activity, and session counts.', + description: 'List all Vibn projects in the workspace (planning records — not live deployments). Use apps_list to see what is actually running.', parameters: { type: 'OBJECT', properties: {}, required: [] }, }, { name: 'projects_get', - description: 'Get detailed information about a specific project by its ID.', + description: 'Get details for a single Vibn project by ID (name, status, vision, linked Coolify UUID).', parameters: { type: 'OBJECT', properties: { - projectId: { type: 'STRING', description: 'The project ID to retrieve.' }, + projectId: { type: 'STRING', description: 'The Vibn project ID.' }, }, required: ['projectId'], }, }, + + // ── Applications ───────────────────────────────────────────────────────── + { name: 'apps_list', - description: 'List all deployed applications in the Coolify workspace.', + description: 'List all live applications and services deployed in the Coolify workspace. Use this (not projects_list) when the user asks what is running or what has a domain.', parameters: { type: 'OBJECT', properties: {}, required: [] }, }, { - name: 'apps_create', - description: 'Create and deploy a new application. Can deploy from a template (e.g. n8n, wordpress, twenty) or a Docker image.', + name: 'apps_get', + description: 'Get full details for a single app: status, domain, git info, build pack, environment.', parameters: { type: 'OBJECT', properties: { - name: { type: 'STRING', description: 'App name (slug-friendly, e.g. "my-app").' }, - domain: { type: 'STRING', description: 'Custom domain (e.g. "myapp.mark.vibnai.com").' }, - template: { type: 'STRING', description: 'Coolify service template name (e.g. "n8n", "wordpress", "twenty").' }, - dockerImage: { type: 'STRING', description: 'Docker image to deploy (e.g. "nginx:latest"). Use instead of template.' }, + uuid: { type: 'STRING', description: 'The Coolify application UUID.' }, }, - required: ['name', 'domain'], + required: ['uuid'], + }, + }, + { + name: 'apps_create', + description: `Create and deploy a new application. Four pathways — pick the right one: +1. template (PREFERRED for popular apps — Twenty, n8n, WordPress, Ghost, Supabase, etc.): pass { template: "" }. Always search apps_templates_search first. +2. image (single Docker container): pass { image: "nginx:latest" }. +3. composeRaw (custom multi-service stack, no template exists): pass { composeRaw: "" }. +4. repo (user's own code in Gitea): pass { repo: "" }. +Auto-domain {name}.{workspace}.vibnai.com is assigned automatically.`, + parameters: { + type: 'OBJECT', + properties: { + name: { type: 'STRING', description: 'App name (slug-friendly, e.g. "my-crm"). Required for all pathways.' }, + domain: { type: 'STRING', description: 'Custom subdomain (e.g. "crm.mark.vibnai.com"). Optional — auto-generated if omitted.' }, + template: { type: 'STRING', description: 'Coolify one-click template slug (e.g. "twenty", "n8n", "wordpress"). Use apps_templates_search to find the slug.' }, + image: { type: 'STRING', description: 'Docker image (e.g. "nginx:latest"). For single-container third-party apps.' }, + composeRaw: { type: 'STRING', description: 'Raw Docker Compose YAML for custom multi-service stacks. Only use when no template exists.' }, + repo: { type: 'STRING', description: 'Gitea repo name (e.g. "my-site") for deploying the user\'s own code.' }, + ports: { type: 'STRING', description: 'Port(s) the app listens on (e.g. "3000"). Required for repo/image pathways.' }, + envs: { type: 'OBJECT', description: 'Environment variables as key-value pairs to inject at creation.' }, + instantDeploy: { type: 'BOOLEAN', description: 'Whether to deploy immediately (default true).' }, + }, + required: ['name'], + }, + }, + { + name: 'apps_update', + description: 'Update whitelisted fields on an existing app (name, description, git branch, ports, build commands, base directory, etc.). Returns applied/ignored/rerouted arrays. Setting domains/git_repository returns a rerouted hint pointing at apps_domains_set or apps_rewire_git.', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify application UUID.' }, + patch: { type: 'OBJECT', description: 'Fields to update as key-value pairs.' }, + }, + required: ['uuid', 'patch'], + }, + }, + { + name: 'apps_rewire_git', + description: 'Re-point an app\'s git_repository at the canonical HTTPS+PAT clone URL. Use to recover older apps created with SSH URLs, or to refresh a rotated bot PAT.', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify application UUID.' }, + repo: { type: 'STRING', description: 'Gitea repo name. Optional — inferred from current URL if omitted.' }, + }, + required: ['uuid'], + }, + }, + { + name: 'apps_delete', + description: 'Destroy an application. Volumes are kept by default. confirm must equal the app\'s exact name.', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify application UUID.' }, + confirm: { type: 'STRING', description: 'Must equal the exact app name (e.g. "my-crm"). Prevents accidental deletion.' }, + }, + required: ['uuid', 'confirm'], + }, + }, + { + name: 'apps_deploy', + description: 'Trigger a new deployment for an existing application.', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify application UUID.' }, + force: { type: 'BOOLEAN', description: 'Force rebuild even if nothing changed.' }, + }, + required: ['uuid'], + }, + }, + { + name: 'apps_deployments', + description: 'List recent deployments for an app with their status (finished, in_progress, failed, queued).', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify application UUID.' }, + }, + required: ['uuid'], + }, + }, + { + name: 'apps_logs', + description: 'Get runtime logs from a running application. Compose-aware: returns per-service logs for multi-service stacks. Use to diagnose crashes, DB errors, or startup failures.', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify application UUID.' }, + service: { type: 'STRING', description: 'For compose apps: specific service name to filter (e.g. "server", "worker"). Omit for all services.' }, + lines: { type: 'NUMBER', description: 'Number of log lines (default 200, max 5000).' }, + }, + required: ['uuid'], + }, + }, + { + name: 'apps_exec', + description: 'Run a one-shot command inside a running app container (via docker exec). Use for database migrations, seeds, CLI invocations, and debugging. Shell syntax works. Default timeout 60s.', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify application UUID.' }, + command: { type: 'STRING', description: 'Shell command to run (e.g. "yarn command:prod database:migrate:prod", "psql $DATABASE_URL -c \'select 1\'").' }, + service: { type: 'STRING', description: 'For compose apps with multiple containers: the service to exec into (e.g. "server", "db").' }, + user: { type: 'STRING', description: 'User to run command as (default: container default user).' }, + workdir: { type: 'STRING', description: 'Working directory inside the container.' }, + timeout_ms: { type: 'NUMBER', description: 'Timeout in milliseconds (default 60000, max 600000).' }, + max_bytes: { type: 'NUMBER', description: 'Max output bytes (default 1MB, max 5MB).' }, + }, + required: ['uuid', 'command'], + }, + }, + { + name: 'apps_volumes_list', + description: 'List Docker volumes belonging to an app (name + size in bytes). Use before apps_volumes_wipe to confirm volume names.', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify application UUID.' }, + }, + required: ['uuid'], + }, + }, + { + name: 'apps_volumes_wipe', + description: 'DESTRUCTIVE. Stop all app containers, remove a specific volume, leave app ready for a fresh deploy. Use to recover from stale DB state on first boot. confirm must equal the exact volume name.', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify application UUID.' }, + volume: { type: 'STRING', description: 'Exact volume name to wipe (get from apps_volumes_list).' }, + confirm: { type: 'STRING', description: 'Must equal the exact volume name to confirm deletion.' }, + }, + required: ['uuid', 'volume', 'confirm'], + }, + }, + { + name: 'apps_containers_up', + description: 'Run docker compose up -d directly on the Coolify host. Use when apps_deploy returned started:false or containers are in Created/Exited state. Idempotent.', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify application UUID.' }, + }, + required: ['uuid'], + }, + }, + { + name: 'apps_containers_ps', + description: 'Run docker compose ps to see container states. Diagnoses Created (queue failure), Exited (crash), Restarting (boot loop), Up healthy/unhealthy.', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify application UUID.' }, + }, + required: ['uuid'], + }, + }, + { + name: 'apps_repair', + description: 'Re-run post-deploy fixes on an existing service: proxy network attach, Traefik label injection, proxy restart. Use when a deploy succeeded but the app returns 502/503 or Mixed Content errors.', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify service UUID.' }, + fqdn: { type: 'STRING', description: 'The public domain the app should be reachable at (e.g. "crm.mark.vibnai.com").' }, + publicAppName: { type: 'STRING', description: 'The name of the public-facing container/service within the stack (e.g. "server", "web").' }, + port: { type: 'NUMBER', description: 'The upstream port the container listens on (e.g. 3000). Optional.' }, + }, + required: ['uuid', 'fqdn', 'publicAppName'], }, }, { name: 'apps_templates_list', - description: 'List available one-click application templates (n8n, wordpress, twenty, etc.).', - parameters: { type: 'OBJECT', properties: {}, required: [] }, - }, - { - name: 'apps_templates_search', - description: 'Search for a specific application template by name or keyword.', + description: 'Browse the Coolify one-click template catalog (320+ apps: CRMs, AI tools, CMSes, dashboards, databases). Each is deployable via apps_create with { template: slug }.', parameters: { type: 'OBJECT', properties: { - query: { type: 'STRING', description: 'Search query (e.g. "crm", "blog", "automation").' }, + limit: { type: 'NUMBER', description: 'Number of templates to return (default 50, max 500).' }, + offset: { type: 'NUMBER', description: 'Pagination offset.' }, + tag: { type: 'STRING', description: 'Filter by tag substring (e.g. "crm", "ai", "database").' }, }, - required: ['query'], + required: [], }, }, { - name: 'domains_register', - description: 'Register a new domain name via OpenSRS.', + name: 'apps_templates_search', + description: 'Search for a Coolify template by name or keyword. Always call this before apps_create to find the correct template slug. Returns ranked matches.', parameters: { type: 'OBJECT', properties: { - domain: { type: 'STRING', description: 'Domain name to register (e.g. "myapp.com").' }, + query: { type: 'STRING', description: 'Search term (e.g. "twenty", "n8n", "ghost blog", "kanban").' }, + tag: { type: 'STRING', description: 'Filter by tag (e.g. "crm", "ai"). Can be used with or without query.' }, + limit: { type: 'NUMBER', description: 'Max results (default 25, max 100).' }, + }, + required: [], + }, + }, + { + name: 'apps_domains_list', + description: 'List the current domain set for an application.', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify application UUID.' }, + }, + required: ['uuid'], + }, + }, + { + name: 'apps_domains_set', + description: 'Replace the domain set for an application. All entries must end with .{workspace}.vibnai.com. For compose apps, use the service parameter to target a specific service.', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify application UUID.' }, + domains: { type: 'ARRAY', description: 'Array of domain strings (e.g. ["myapp.mark.vibnai.com", "api.mark.vibnai.com"]).' }, + service: { type: 'STRING', description: 'For compose apps: the service to attach the domain to (e.g. "server"). Default: "server".' }, + }, + required: ['uuid', 'domains'], + }, + }, + { + name: 'apps_envs_list', + description: 'List environment variables for an application. Secret values (is_shown_once) are redacted.', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify application UUID.' }, + }, + required: ['uuid'], + }, + }, + { + name: 'apps_envs_upsert', + description: 'Create or update a single environment variable on an application.', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify application UUID.' }, + key: { type: 'STRING', description: 'Env var key (uppercase, e.g. "DATABASE_URL").' }, + value: { type: 'STRING', description: 'Env var value.' }, + isShownOnce: { type: 'BOOLEAN', description: 'If true, value is write-only after creation (for secrets). Default false.' }, + isMultiline: { type: 'BOOLEAN', description: 'If true, value spans multiple lines.' }, + }, + required: ['uuid', 'key', 'value'], + }, + }, + { + name: 'apps_envs_delete', + description: 'Delete an environment variable from an application.', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify application UUID.' }, + key: { type: 'STRING', description: 'Env var key to delete.' }, + }, + required: ['uuid', 'key'], + }, + }, + + // ── Databases ───────────────────────────────────────────────────────────── + + { + name: 'databases_list', + description: 'List all databases in the workspace across all flavors (Postgres, MySQL, Redis, MongoDB, etc.).', + parameters: { type: 'OBJECT', properties: {}, required: [] }, + }, + { + name: 'databases_create', + description: 'Provision a new database. Supported types: postgresql, mysql, mariadb, mongodb, redis, keydb, dragonfly, clickhouse.', + parameters: { + type: 'OBJECT', + properties: { + type: { type: 'STRING', description: 'Database type: "postgresql", "mysql", "mariadb", "mongodb", "redis", "keydb", "dragonfly", or "clickhouse".' }, + name: { type: 'STRING', description: 'Database name (slug-friendly). Auto-generated if omitted.' }, + isPublic: { type: 'BOOLEAN', description: 'Whether to expose a public port. Default false (internal only).' }, + publicPort: { type: 'NUMBER', description: 'Public port number if isPublic is true.' }, + }, + required: ['type'], + }, + }, + { + name: 'databases_get', + description: 'Get details for a database including the internal connection URL.', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify database UUID.' }, + }, + required: ['uuid'], + }, + }, + { + name: 'databases_update', + description: 'Update database settings: name, public visibility, image version, resource limits.', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify database UUID.' }, + patch: { type: 'OBJECT', description: 'Fields to update.' }, + }, + required: ['uuid', 'patch'], + }, + }, + { + name: 'databases_delete', + description: 'Destroy a database. Volumes kept by default. confirm must equal the database\'s exact name.', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify database UUID.' }, + confirm: { type: 'STRING', description: 'Must equal the exact database name to confirm deletion.' }, + }, + required: ['uuid', 'confirm'], + }, + }, + + // ── Auth providers ──────────────────────────────────────────────────────── + + { + name: 'auth_list', + description: 'List deployed authentication providers in the workspace plus the allowlist of supported providers.', + parameters: { type: 'OBJECT', properties: {}, required: [] }, + }, + { + name: 'auth_create', + description: 'Deploy an auth provider from the allowlist. Supported providers: pocketbase, authentik, keycloak, keycloak-with-postgres, pocket-id, pocket-id-with-postgresql, logto, supertokens-with-postgresql.', + parameters: { + type: 'OBJECT', + properties: { + provider: { type: 'STRING', description: 'Provider key (e.g. "pocketbase", "authentik", "keycloak").' }, + name: { type: 'STRING', description: 'Instance name. Auto-generated if omitted.' }, + description: { type: 'STRING', description: 'Optional description.' }, + instantDeploy: { type: 'BOOLEAN', description: 'Deploy immediately (default true).' }, + }, + required: ['provider'], + }, + }, + { + name: 'auth_delete', + description: 'Destroy an auth provider. User data volumes are kept by default. confirm must equal the service\'s exact name.', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify service UUID.' }, + confirm: { type: 'STRING', description: 'Must equal the exact service name to confirm deletion.' }, + }, + required: ['uuid', 'confirm'], + }, + }, + + // ── Domains ─────────────────────────────────────────────────────────────── + + { + name: 'domains_search', + description: 'Check availability and pricing for domain names via OpenSRS. Stateless — does not reserve anything.', + parameters: { + type: 'OBJECT', + properties: { + names: { type: 'ARRAY', description: 'Array of domain names to check (e.g. ["myapp.com", "myapp.io"]). Max 25.' }, + period: { type: 'NUMBER', description: 'Registration period in years (default 1). Note: .ai requires 2 years minimum.' }, + }, + required: ['names'], + }, + }, + { + name: 'domains_list', + description: 'List all domains owned by the workspace with their status, registrar order ID, expiry, and DNS provider.', + parameters: { type: 'OBJECT', properties: {}, required: [] }, + }, + { + name: 'domains_get', + description: 'Get full record and last 20 lifecycle events for a specific domain.', + parameters: { + type: 'OBJECT', + properties: { + domain: { type: 'STRING', description: 'The domain name (e.g. "myapp.com").' }, }, required: ['domain'], }, }, { - name: 'domains_list', - description: 'List all domains registered or attached in the workspace.', - parameters: { type: 'OBJECT', properties: {}, required: [] }, - }, - { - name: 'apps_logs', - description: 'Get recent runtime logs from a deployed application.', + name: 'domains_register', + description: 'Register a domain through OpenSRS. Idempotent per (workspace, domain). Confirm availability with domains_search first.', parameters: { type: 'OBJECT', properties: { - appUuid: { type: 'STRING', description: 'The Coolify application UUID.' }, - lines: { type: 'NUMBER', description: 'Number of log lines to return (default 50).' }, + domain: { type: 'STRING', description: 'Domain name to register (e.g. "myapp.com").' }, + period: { type: 'NUMBER', description: 'Registration period in years (default 1).' }, + whoisPrivacy: { type: 'BOOLEAN', description: 'Enable WHOIS privacy (default true).' }, }, - required: ['appUuid'], + required: ['domain'], }, }, + { + name: 'domains_attach', + description: 'Wire a registered domain to a Coolify app: creates Cloud DNS zone, writes A/CNAME records, updates registrar nameservers, appends domain to Coolify app. Idempotent.', + parameters: { + type: 'OBJECT', + properties: { + domain: { type: 'STRING', description: 'The registered domain name (e.g. "myapp.com").' }, + appUuid: { type: 'STRING', description: 'Coolify app UUID to attach the domain to.' }, + subdomains: { type: 'ARRAY', description: 'Subdomains to wire (default ["@", "www"]).' }, + }, + required: ['domain'], + }, + }, + + // ── Storage ─────────────────────────────────────────────────────────────── + + { + name: 'storage_describe', + description: 'Report workspace GCS bucket name, region, S3-compatible endpoint, and access key ID. No secret returned.', + parameters: { type: 'OBJECT', properties: {}, required: [] }, + }, + { + name: 'storage_provision', + description: 'Idempotently create or reconcile the workspace GCS bucket, service account, IAM binding, and HMAC key. Safe to re-run.', + parameters: { type: 'OBJECT', properties: {}, required: [] }, + }, + { + name: 'storage_inject_env', + description: 'Push STORAGE_* env vars (endpoint, region, bucket, access key, secret) into a Coolify app. Secret is written server-side and never returned in the response.', + parameters: { + type: 'OBJECT', + properties: { + uuid: { type: 'STRING', description: 'The Coolify application UUID to inject storage credentials into.' }, + prefix: { type: 'STRING', description: 'Env var prefix (default "STORAGE_"; use "S3_" for AWS-standard names).' }, + }, + required: ['uuid'], + }, + }, + + // ── Non-MCP: GitHub & web ───────────────────────────────────────────────── + { name: 'github_search', description: - 'Search GitHub for public repositories. Use this to find open source projects, ' + - 'reference implementations, design systems, or starting points. ' + - 'Always add "license:mit" to the query to ensure permissive licensing. ' + - 'Example queries: "license:mit self-hosted crm", "license:mit kanban board react", ' + - '"license:mit design-system components".', + 'Search GitHub for public repositories. Use to find open source reference projects, design systems, or starting points. ' + + 'Add "license:mit" to ensure permissive licensing. ' + + 'Example queries: "license:mit self-hosted crm", "license:mit kanban react", "license:mit design-system components".', parameters: { type: 'OBJECT', properties: { query: { type: 'STRING', - description: - 'GitHub search query. Include license:mit unless you have a reason not to. ' + - 'You can filter by language (language:typescript), stars (stars:>500), ' + - 'topics (topic:self-hosted), and file presence (filename:docker-compose.yml).', - }, - sort: { - type: 'STRING', - description: 'Sort by: "stars" (default), "updated", or "forks".', - }, - limit: { - type: 'NUMBER', - description: 'Number of results to return (default 8, max 20).', + description: 'GitHub search query. Include license:mit unless intentionally looking for non-MIT. ' + + 'Supports: language:typescript, stars:>500, topic:self-hosted, filename:docker-compose.yml.', }, + sort: { type: 'STRING', description: 'Sort by: "stars" (default), "updated", or "forks".' }, + limit: { type: 'NUMBER', description: 'Results to return (default 8, max 20).' }, }, required: ['query'], }, @@ -120,26 +524,14 @@ export const VIBN_TOOL_DEFINITIONS: ToolDefinition[] = [ { name: 'github_file', description: - 'Read a specific file from a public GitHub repository. ' + - 'Use this to study a project\'s design system, component library, README, ' + - 'package.json, docker-compose.yml, or any other file. ' + - 'Great for understanding how an existing open source project is structured ' + - 'before building something inspired by it.', + 'Read a specific file from a public GitHub repository. Use to study design systems, component libraries, ' + + 'README files, package.json, docker-compose.yml, or any file in an open source project.', parameters: { type: 'OBJECT', properties: { - repo: { - type: 'STRING', - description: 'Repository in "owner/repo" format (e.g. "makeplane/plane").', - }, - path: { - type: 'STRING', - description: 'File path within the repo (e.g. "README.md", "packages/ui/src/index.ts", "docker-compose.yml").', - }, - ref: { - type: 'STRING', - description: 'Branch or commit ref (default: "main").', - }, + repo: { type: 'STRING', description: 'Repository in "owner/repo" format (e.g. "makeplane/plane").' }, + path: { type: 'STRING', description: 'File path within the repo (e.g. "README.md", "docker-compose.yml").' }, + ref: { type: 'STRING', description: 'Branch or commit ref (default: "main").' }, }, required: ['repo', 'path'], }, @@ -147,30 +539,27 @@ export const VIBN_TOOL_DEFINITIONS: ToolDefinition[] = [ { name: 'http_fetch', description: - 'Fetch any public URL and return the response body as text. ' + - 'Use for reading documentation, API responses, public datasets, ' + - 'or any web resource relevant to the user\'s request. ' + - 'Response is truncated to 12KB to fit context.', + 'Fetch any public URL and return the response body as text. Use for reading documentation, ' + + 'API responses, or any public web resource. Response truncated to 12KB.', parameters: { type: 'OBJECT', properties: { - url: { - type: 'STRING', - description: 'The full URL to fetch (must be publicly accessible, https preferred).', - }, - headers: { - type: 'OBJECT', - description: 'Optional HTTP headers as key-value pairs.', - }, + url: { type: 'STRING', description: 'The full URL to fetch (https preferred).' }, + headers: { type: 'OBJECT', description: 'Optional HTTP headers as key-value pairs.' }, }, required: ['url'], }, }, ]; +// ── Tool execution ──────────────────────────────────────────────────────────── + +const NON_MCP_TOOLS = new Set(['github_search', 'github_file', 'http_fetch']); + /** - * Execute a Vibn MCP tool call by forwarding to the internal MCP API. - * Called server-side from the /api/chat route, using the user's workspace API key. + * Execute any Vibn tool. Non-MCP tools (GitHub, http_fetch) run locally. + * All MCP tools forward to POST /api/mcp — the tool name maps to the MCP + * action by replacing underscores with dots (e.g. apps_create → apps.create). */ export async function executeMcpTool( toolName: string, @@ -178,28 +567,12 @@ export async function executeMcpTool( mcpToken: string, baseUrl: string, ): Promise { - // Handle non-MCP tools directly if (toolName === 'github_search') return executeGithubSearch(args); if (toolName === 'github_file') return executeGithubFile(args); if (toolName === 'http_fetch') return executeHttpFetch(args); - // Map tool names (underscored) back to MCP action names (dotted) - const actionMap: Record = { - projects_list: 'projects.list', - projects_get: 'projects.get', - apps_list: 'apps.list', - apps_create: 'apps.create', - apps_templates_list: 'apps.templates.list', - apps_templates_search: 'apps.templates.search', - domains_register: 'domains.register', - domains_list: 'domains.list', - apps_logs: 'apps.logs', - }; - - const action = actionMap[toolName]; - if (!action) { - return JSON.stringify({ error: `Unknown tool: ${toolName}` }); - } + // Convert underscore tool name → dotted MCP action (apps_create → apps.create) + const action = toolName.replace(/_/g, '.'); try { const res = await fetch(`${baseUrl}/api/mcp`, { @@ -217,7 +590,7 @@ export async function executeMcpTool( } } -// ── Non-MCP tool implementations ────────────────────────────────────────────── +// ── Non-MCP implementations ─────────────────────────────────────────────────── async function executeGithubSearch(args: Record): Promise { const query = String(args.query || ''); @@ -225,12 +598,7 @@ async function executeGithubSearch(args: Record): Promise = { Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28', @@ -239,7 +607,6 @@ async function executeGithubSearch(args: Record): Promise ({ @@ -249,7 +616,6 @@ async function executeGithubSearch(args: Record): Promise): Promise }; if (GITHUB_TOKEN) headers['Authorization'] = `Bearer ${GITHUB_TOKEN}`; - const res = await fetch( - `https://api.github.com/repos/${repo}/contents/${path}?ref=${ref}`, - { headers }, - ); - + const res = await fetch(`https://api.github.com/repos/${repo}/contents/${path}?ref=${ref}`, { headers }); if (!res.ok) { - // Try alternate branch if main fails if (ref === 'main') { - const res2 = await fetch( - `https://api.github.com/repos/${repo}/contents/${path}?ref=master`, - { headers }, - ); - if (res2.ok) { - const text = await res2.text(); - return text.slice(0, 12000); - } + const res2 = await fetch(`https://api.github.com/repos/${repo}/contents/${path}?ref=master`, { headers }); + if (res2.ok) return (await res2.text()).slice(0, 12000); } return JSON.stringify({ error: `File not found: ${repo}/${path} (ref: ${ref})` }); } - - const text = await res.text(); - // GitHub returns base64-encoded content for files; raw+json accept header gets raw text - return text.slice(0, 12000); + return (await res.text()).slice(0, 12000); } catch (e) { return JSON.stringify({ error: e instanceof Error ? e.message : String(e) }); } @@ -311,23 +663,13 @@ async function executeHttpFetch(args: Record): Promise try { const res = await fetch(url, { - headers: { - 'User-Agent': 'Vibn-AI/1.0', - ...extraHeaders, - }, + headers: { 'User-Agent': 'Vibn-AI/1.0', ...extraHeaders }, signal: AbortSignal.timeout(10_000), }); - const contentType = res.headers.get('content-type') || ''; - let body: string; - - if (contentType.includes('json')) { - const json = await res.json(); - body = JSON.stringify(json, null, 2); - } else { - body = await res.text(); - } - + const body = contentType.includes('json') + ? JSON.stringify(await res.json(), null, 2) + : await res.text(); return `HTTP ${res.status}\nContent-Type: ${contentType}\n\n${body}`.slice(0, 12000); } catch (e) { return JSON.stringify({ error: e instanceof Error ? e.message : String(e) });