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:
143
services/openclaw-mcp-server.mjs
Normal file
143
services/openclaw-mcp-server.mjs
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user