#!/usr/bin/env node /** * Zed MCP bridge — translates JSON-RPC 2.0 (Zed's MCP client) to Vibn's * simpler {tool, params} HTTP API at /api/mcp. * * Usage: * node mcp-zed-bridge.js * * Env: * VIBN_MCP_URL — defaults to https://vibnai.com/api/mcp * VIBN_API_KEY — your vibn_sk_... token */ const VIBNDEV_MCP_URL = process.env.VIBN_MCP_URL || "https://vibnai.com/api/mcp"; const VIBNDEV_API_KEY = process.env.VIBN_API_KEY || ""; if (!VIBNDEV_API_KEY) { process.stderr.write("VIBN_API_KEY is required\n"); process.exit(1); } // ── JSON-RPC helpers ───────────────────────────────────────────────── let requestId = 0; function sendJson(obj) { process.stdout.write(JSON.stringify(obj) + "\n"); } function log(msg) { process.stderr.write(`[vibn-bridge] ${msg}\n`); } // ── Tool list from the API capability descriptor ────────────────────── const TOOLS = [ "workspace.describe", "gitea.credentials", "projects.list", "projects.get", "project.recent_errors", "project.error_detail", "project.error_resolve", "apps.list", "apps.get", "apps.create", "apps.update", "apps.rewire_git", "apps.delete", "apps.deploy", "apps.deployments", "apps.domains.list", "apps.domains.set", "apps.logs", "apps.exec", "apps.volumes.list", "apps.volumes.wipe", "apps.containers.up", "apps.containers.ps", "apps.repair", "apps.templates.list", "apps.templates.search", "apps.envs.list", "apps.envs.upsert", "apps.envs.delete", "databases.list", "databases.create", "databases.get", "databases.update", "databases.delete", "auth.list", "auth.create", "auth.delete", "domains.search", "domains.list", "domains.get", "domains.register", "domains.attach", "storage.describe", "storage.provision", "storage.inject_env", "gitea.repos.list", "gitea.repo.get", "gitea.repo.create", "gitea.file.read", "gitea.file.write", "gitea.file.delete", "gitea.branches.list", "gitea.branch.create", "devcontainer.ensure", "devcontainer.status", "devcontainer.suspend", "shell.exec", "fs.read", "fs.write", "fs.edit", "fs.list", "fs.tree", "fs.delete", "fs.glob", "fs.grep", "dev_server.start", "browser.console", "browser.navigate", "dev_server.stop", "dev_server.list", "dev_server.logs", "ship", ].map((name) => ({ name, description: `Vibn workspace tool: ${name}`, inputSchema: { type: "object", properties: { projectId: { type: "string", description: "Vibn project ID" }, }, }, })); // ── MCP protocol handlers ──────────────────────────────────────────── async function handleInitialize(id) { sendJson({ jsonrpc: "2.0", id, result: { protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: "vibn-mcp-bridge", version: "1.0.0" }, }, }); } async function handleToolsList(id) { sendJson({ jsonrpc: "2.0", id, result: { tools: TOOLS }, }); } async function handleToolsCall(id, params) { const { name, arguments: args } = params; try { const res = await fetch(VIBNDEV_MCP_URL, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${VIBNDEV_API_KEY}`, }, body: JSON.stringify({ tool: name, params: args }), }); const data = await res.json(); if (!res.ok) { sendJson({ jsonrpc: "2.0", id, result: { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], isError: true, }, }); return; } sendJson({ jsonrpc: "2.0", id, result: { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }, }); } catch (err) { sendJson({ jsonrpc: "2.0", id, result: { content: [ { type: "text", text: `Error: ${err.message}`, }, ], isError: true, }, }); } } // ── Main loop ───────────────────────────────────────────────────────── let buffer = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (chunk) => { buffer += chunk; const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (!line.trim()) continue; let msg; try { msg = JSON.parse(line); } catch { continue; } if (!msg.jsonrpc || !msg.method) continue; const id = msg.id; // Wrap in async IIFE so unhandled rejections don't crash the bridge (async () => { try { switch (msg.method) { case "initialize": await handleInitialize(id); break; case "notifications/initialized": // No response needed break; case "tools/list": await handleToolsList(id); break; case "tools/call": await handleToolsCall(id, msg.params); break; default: sendJson({ jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${msg.method}`, }, }); } } catch (err) { log(`Error handling ${msg.method}: ${err.message}`); sendJson({ jsonrpc: "2.0", id, error: { code: -32603, message: `Internal error: ${err.message}` }, }); } })(); } }); process.stdin.on("end", () => { log("stdin closed, exiting"); process.exit(0); }); log(`bridge started, proxying to ${VIBNDEV_MCP_URL}`);