Files
portable-config/templates/scripts/openviking-mcp.js
T
2026-03-31 19:02:14 -04:00

207 lines
5.6 KiB
JavaScript
Executable File

#!/usr/bin/env node
"use strict";
const BASE_URL = (process.env.OPENVIKING_BASE_URL || "http://127.0.0.1:1933").replace(/\/+$/, "");
const SEARCH_PATHS = (process.env.OPENVIKING_SEARCH_PATHS || "/api/v1/search,/find")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
const READ_PATHS = (process.env.OPENVIKING_READ_PATHS || "/api/v1/read,/read")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
const UPSERT_PATHS = (process.env.OPENVIKING_UPSERT_PATHS || "/api/v1/add_resource,/add_resource")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
function writeMessage(msg) {
const body = Buffer.from(JSON.stringify(msg), "utf8");
process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
process.stdout.write(body);
}
async function postWithFallback(paths, payload) {
let lastError = null;
for (const path of paths) {
try {
const res = await fetch(`${BASE_URL}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) {
const text = await res.text();
lastError = new Error(`HTTP ${res.status} on ${path}: ${text.slice(0, 400)}`);
continue;
}
return await res.json();
} catch (err) {
lastError = err;
}
}
throw lastError || new Error("All endpoint attempts failed");
}
function makeTools() {
return [
{
name: "openviking_search",
description: "Search OpenViking memory/context and return top matches.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search query." },
top_k: { type: "integer", minimum: 1, maximum: 20, default: 5 },
namespace: { type: "string", description: "Optional namespace/project bucket." },
},
required: ["query"],
},
},
{
name: "openviking_read",
description: "Read a resource from OpenViking by URI or ID.",
inputSchema: {
type: "object",
properties: {
uri: { type: "string" },
id: { type: "string" },
namespace: { type: "string" },
},
},
},
{
name: "openviking_upsert",
description: "Add or update a memory resource in OpenViking.",
inputSchema: {
type: "object",
properties: {
id: { type: "string" },
uri: { type: "string" },
title: { type: "string" },
content: { type: "string" },
namespace: { type: "string" },
metadata: { type: "object" },
},
required: ["content"],
},
},
];
}
async function handleToolCall(name, args) {
if (name === "openviking_search") {
const payload = {
query: args.query,
top_k: args.top_k ?? 5,
namespace: args.namespace,
};
const data = await postWithFallback(SEARCH_PATHS, payload);
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
if (name === "openviking_read") {
const payload = {
uri: args.uri,
id: args.id,
namespace: args.namespace,
};
const data = await postWithFallback(READ_PATHS, payload);
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
if (name === "openviking_upsert") {
const payload = {
id: args.id,
uri: args.uri,
title: args.title,
content: args.content,
namespace: args.namespace,
metadata: args.metadata,
};
const data = await postWithFallback(UPSERT_PATHS, payload);
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
throw new Error(`Unknown tool: ${name}`);
}
async function onRequest(msg) {
const { id, method, params } = msg;
try {
if (method === "initialize") {
writeMessage({
jsonrpc: "2.0",
id,
result: {
protocolVersion: "2024-11-05",
serverInfo: { name: "openviking-mcp-bridge", version: "0.1.0" },
capabilities: { tools: {} },
},
});
return;
}
if (method === "notifications/initialized") {
return;
}
if (method === "tools/list") {
writeMessage({
jsonrpc: "2.0",
id,
result: { tools: makeTools() },
});
return;
}
if (method === "tools/call") {
const toolName = params?.name;
const args = params?.arguments || {};
const result = await handleToolCall(toolName, args);
writeMessage({ jsonrpc: "2.0", id, result });
return;
}
writeMessage({
jsonrpc: "2.0",
id,
error: { code: -32601, message: `Method not found: ${method}` },
});
} catch (err) {
writeMessage({
jsonrpc: "2.0",
id,
error: { code: -32000, message: err && err.message ? err.message : String(err) },
});
}
}
let buffer = Buffer.alloc(0);
process.stdin.on("data", (chunk) => {
buffer = Buffer.concat([buffer, chunk]);
while (true) {
const headerEnd = buffer.indexOf("\r\n\r\n");
if (headerEnd === -1) break;
const headerText = buffer.slice(0, headerEnd).toString("utf8");
const match = headerText.match(/Content-Length:\s*(\d+)/i);
if (!match) {
buffer = buffer.slice(headerEnd + 4);
continue;
}
const length = Number(match[1]);
const total = headerEnd + 4 + length;
if (buffer.length < total) break;
const body = buffer.slice(headerEnd + 4, total).toString("utf8");
buffer = buffer.slice(total);
let msg;
try {
msg = JSON.parse(body);
} catch {
continue;
}
if (msg && msg.jsonrpc === "2.0" && msg.method) {
onRequest(msg);
}
}
});