Files

268 lines
6.0 KiB
JavaScript
Executable File

#!/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 ─────────────────────────────────────────────────
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}`);