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
This commit is contained in:
2026-04-27 17:55:57 -07:00
parent bea23cf307
commit c4ef30066f
2 changed files with 499 additions and 155 deletions

View File

@@ -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' })}.`;
}

View File

@@ -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: "<slug>" }. 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: "<yaml>" }.
4. repo (user's own code in Gitea): pass { repo: "<repo-name>" }.
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<string> {
// 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<string, string> = {
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<string, unknown>): Promise<string> {
const query = String(args.query || '');
@@ -225,12 +598,7 @@ async function executeGithubSearch(args: Record<string, unknown>): Promise<strin
const limit = Math.min(Number(args.limit || 8), 20);
try {
const params = new URLSearchParams({
q: query,
sort,
order: 'desc',
per_page: String(limit),
});
const params = new URLSearchParams({ q: query, sort, order: 'desc', per_page: String(limit) });
const headers: Record<string, string> = {
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
@@ -239,7 +607,6 @@ async function executeGithubSearch(args: Record<string, unknown>): Promise<strin
const res = await fetch(`https://api.github.com/search/repositories?${params}`, { headers });
const data = await res.json();
if (!res.ok) return JSON.stringify({ error: data.message || 'GitHub API error' });
const repos = (data.items || []).map((r: any) => ({
@@ -249,7 +616,6 @@ async function executeGithubSearch(args: Record<string, unknown>): Promise<strin
language: r.language,
license: r.license?.spdx_id,
topics: r.topics,
hasDockerfile: r.topics?.includes('docker') || false,
updatedAt: r.pushed_at?.slice(0, 10),
url: r.html_url,
defaultBranch: r.default_branch,
@@ -273,29 +639,15 @@ async function executeGithubFile(args: Record<string, unknown>): Promise<string>
};
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<string, unknown>): Promise<string>
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) });