- Migrate src/ → plugin/ (plugin/core/, plugin/web/, plugin/commands/)
and src/mcp/ → services/ per OpenClaw plugin dev spec
- Add Gemini CLI backend (plugin/core/gemini/sdk-adapter.ts) with GEMINI.md
system-prompt injection
- Inject bootstrap as stateless system prompt on every turn instead of
first turn only: Claude via --system-prompt, Gemini via workspace/GEMINI.md;
eliminates isFirstTurn branch, keeps skills in sync with OpenClaw snapshots
- Fix session-map-store defensive parsing (sessions ?? []) to handle bare {}
reset files without crashing on .find()
- Add docs/TEST_FLOW.md with E2E test scenarios and expected outcomes
- Add docs/claude/BRIDGE_MODEL_FINDINGS.md with contractor-probe results
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
144 lines
4.5 KiB
JavaScript
144 lines
4.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* OpenClaw MCP proxy server (stdio transport).
|
|
*
|
|
* Reads OpenClaw tool definitions from a JSON file (TOOL_DEFS_FILE env var),
|
|
* exposes them to Claude Code as MCP tools, and executes them by calling back
|
|
* to the bridge's /mcp/execute HTTP endpoint (BRIDGE_EXECUTE_URL env var).
|
|
*
|
|
* Started per-session by sdk-adapter.ts via --mcp-config.
|
|
*/
|
|
|
|
import fs from "node:fs";
|
|
import readline from "node:readline";
|
|
|
|
// ── Load tool definitions ─────────────────────────────────────────────────────
|
|
|
|
function loadToolDefs() {
|
|
const path = process.env.TOOL_DEFS_FILE;
|
|
if (!path) return [];
|
|
try {
|
|
return JSON.parse(fs.readFileSync(path, "utf8"));
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// ── MCP stdio transport (newline-delimited JSON-RPC 2.0) ──────────────────────
|
|
|
|
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
|
|
function send(msg) {
|
|
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
}
|
|
|
|
function sendResult(id, result) {
|
|
send({ jsonrpc: "2.0", id, result });
|
|
}
|
|
|
|
function sendError(id, code, message) {
|
|
send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
}
|
|
|
|
// ── Tool execution via bridge HTTP ────────────────────────────────────────────
|
|
|
|
async function executeTool(name, args) {
|
|
const url = process.env.BRIDGE_EXECUTE_URL;
|
|
const apiKey = process.env.BRIDGE_API_KEY ?? "";
|
|
const workspace = process.env.WORKSPACE ?? "";
|
|
const agentId = process.env.AGENT_ID ?? "";
|
|
if (!url) return `[mcp-proxy] BRIDGE_EXECUTE_URL not configured`;
|
|
|
|
try {
|
|
const resp = await fetch(url, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${apiKey}`,
|
|
},
|
|
// Include workspace and agentId so bridge can build the correct tool execution context
|
|
body: JSON.stringify({ tool: name, args, workspace, agentId }),
|
|
});
|
|
const data = await resp.json();
|
|
if (data.error) return `[tool error] ${data.error}`;
|
|
return data.result ?? "(no result)";
|
|
} catch (err) {
|
|
return `[mcp-proxy fetch error] ${String(err)}`;
|
|
}
|
|
}
|
|
|
|
// ── Request dispatcher ────────────────────────────────────────────────────────
|
|
|
|
let toolDefs = [];
|
|
|
|
async function handleRequest(msg) {
|
|
const id = msg.id ?? null;
|
|
const method = msg.method;
|
|
const params = msg.params ?? {};
|
|
|
|
if (method === "initialize") {
|
|
toolDefs = loadToolDefs();
|
|
sendResult(id, {
|
|
protocolVersion: "2024-11-05",
|
|
capabilities: { tools: {} },
|
|
serverInfo: { name: "openclaw-mcp-proxy", version: "0.1.0" },
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (method === "notifications/initialized") {
|
|
return; // no response for notifications
|
|
}
|
|
|
|
if (method === "tools/list") {
|
|
const tools = toolDefs.map((t) => {
|
|
const originName = t.function.name;
|
|
const baseDesc = t.function.description ?? "";
|
|
const aliasNote = `[openclaw tool: ${originName}] If any skill or instruction refers to "${originName}", this is the tool to call.`;
|
|
const description = baseDesc ? `${baseDesc}\n${aliasNote}` : aliasNote;
|
|
return {
|
|
name: originName,
|
|
description,
|
|
inputSchema: t.function.parameters ?? { type: "object", properties: {} },
|
|
};
|
|
});
|
|
sendResult(id, { tools });
|
|
return;
|
|
}
|
|
|
|
if (method === "tools/call") {
|
|
const toolName = params.name;
|
|
const toolArgs = params.arguments ?? {};
|
|
try {
|
|
const result = await executeTool(toolName, toolArgs);
|
|
sendResult(id, {
|
|
content: [{ type: "text", text: result }],
|
|
isError: false,
|
|
});
|
|
} catch (err) {
|
|
sendResult(id, {
|
|
content: [{ type: "text", text: `[mcp-proxy] ${String(err)}` }],
|
|
isError: true,
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
sendError(id, -32601, `Method not found: ${method}`);
|
|
}
|
|
|
|
// ── Main loop ─────────────────────────────────────────────────────────────────
|
|
|
|
rl.on("line", async (line) => {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) return;
|
|
let msg;
|
|
try {
|
|
msg = JSON.parse(trimmed);
|
|
} catch {
|
|
sendError(null, -32700, "Parse error");
|
|
return;
|
|
}
|
|
await handleRequest(msg);
|
|
});
|