refactor: restructure to plugin/ + services/ layout and add per-turn bootstrap injection

- 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>
This commit is contained in:
h z
2026-04-11 21:21:32 +01:00
parent eee62efbf1
commit 07a0f06e2e
30 changed files with 1239 additions and 172 deletions

View File

@@ -0,0 +1,143 @@
#!/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);
});