openai: shared MCP layer + openclaw-tools bridge [Adolf P5]
- adolf-llm/server.js now loads SHARED_MCP_SERVERS from the mounted
/shared-mcp.json instead of a hardcoded stub, so adding a shared MCP
server is a one-file change. Verified end-to-end: a real chat-completions
turn writes a session .mcp.json containing both cognee and openclaw-tools
entries (kimi itself still needs `kimi login` in adolf-llm-home, unrelated
to this change).
- Documented the Gate-1 transport reconciliation: decompiled the installed
@moonshot-ai/kimi-code package to confirm its .mcp.json schema keys remote
servers on `transport` ("stdio"/"http"/"sse", inferred as "http" from a
bare `url`, never "sse"), while OpenClaw's own canonical mcp.servers schema
uses different literals ("streamable-http"/"sse") for the same field name
and treats `type` as a CLI-native alias it normalizes itself. `type: "http"`
is the one shape both consumers tolerate, so shared-mcp.json keeps it.
- New openai/openclaw-tools/ service: a stateless MCP-over-Streamable-HTTP
bridge (Node, @modelcontextprotocol/sdk) exposing message_send, cron_create,
cron_list, nodes_invoke, and browser_invoke, each proxying to the OpenClaw
gateway's POST /tools/invoke. Verified initialize + tools/list handshake and
a tools/call against the not-yet-running `adolf` gateway returns a clean
isError content instead of breaking the MCP connection. Documented that
cron/nodes are hard-denied on that HTTP surface by default until P6 adds
them to gateway.tools.allow; message/browser are not similarly restricted.
- Wired openclaw-tools into docker-compose.yml (openai network, :8020) and
added its shared-mcp.json entry alongside cognee.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
@@ -19,21 +19,45 @@ fs.mkdirSync(STATE_DIR, { recursive: true });
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared MCP layer (Gate 1). Kimi Code CLI has NO `--mcp-config-file` flag and
|
||||
// no `kimi mcp` subcommand; it auto-discovers a project-root `.mcp.json`
|
||||
// (Claude-Code-compatible schema) by walking up from its cwd. So we drop a
|
||||
// `.mcp.json` into each session's working directory before spawning kimi.
|
||||
// no `kimi mcp` subcommand; it auto-discovers a project-root `.mcp.json` by
|
||||
// walking up from its cwd to the nearest `.git` (falling back to cwd itself
|
||||
// when none is found). So we drop a `.mcp.json` into each session's working
|
||||
// directory before spawning kimi.
|
||||
//
|
||||
// STUB: the server list is empty for now. cognee-mcp (memory_search/add/cognify,
|
||||
// P4) and the openclaw-tools bridge (P5) do not exist yet. Wiring them later is
|
||||
// a one-line change here — add entries to SHARED_MCP_SERVERS and every future
|
||||
// session dir picks them up automatically. Schema per server, e.g.:
|
||||
// cognee: { command, args, env } (stdio)
|
||||
// openclaw-tools:{ type: "sse"|"http", url, headers } (remote)
|
||||
// TODO(P4/P5): populate SHARED_MCP_SERVERS from cognee-mcp + openclaw-tools.
|
||||
const SHARED_MCP_SERVERS = {
|
||||
// TODO(P4): "cognee": { command: "...", args: [...], env: {...} },
|
||||
// TODO(P5): "openclaw-tools": { type: "sse", url: "http://openclaw:.../mcp" },
|
||||
};
|
||||
// Single source of truth: `/shared-mcp.json` (mounted read-only from the repo
|
||||
// root's `shared-mcp.json`, the same file P6 wires into OpenClaw's own
|
||||
// `mcp.servers` registry). Adding a server is then a one-file change — no
|
||||
// server list is hardcoded here anymore.
|
||||
//
|
||||
// Gate-1 transport finding (P5, verified by decompiling the installed
|
||||
// @moonshot-ai/kimi-code package, packages/agent-core/src/config/schema.ts's
|
||||
// McpServerConfigSchema): Kimi's own field name for remote MCP servers is
|
||||
// `transport` (literal "stdio" | "http" | "sse"), not `type`. When `transport`
|
||||
// is omitted, Kimi's config preprocessor infers it from shape: `command` ->
|
||||
// "stdio", `url` -> "http" (never "sse" — sse requires an explicit
|
||||
// `transport: "sse"`). It does NOT recognize a `type` key at all; unknown keys
|
||||
// are silently stripped by the (non-strict) zod schema.
|
||||
// OpenClaw's own canonical `mcp.servers` schema (docs/gateway/
|
||||
// configuration-reference.md) uses different literals for the same
|
||||
// transport: `transport: "streamable-http"` or `"sse"`, with `type: "http"`
|
||||
// documented as a *CLI-native alias* that `openclaw mcp set` / `openclaw
|
||||
// doctor --fix` normalize into canonical `transport: "streamable-http"`.
|
||||
// So the two consumers disagree on the literal value for HTTP streaming
|
||||
// ("http" vs "streamable-http") under the same field name `transport` --
|
||||
// writing `transport` explicitly in shared-mcp.json would satisfy at most one
|
||||
// side. `type: "http"` is the one shape both sides tolerate today: Kimi
|
||||
// ignores the unrecognized `type` key and correctly infers transport "http"
|
||||
// from the `url` field alone; OpenClaw recognizes `type` as its documented
|
||||
// alias and normalizes it on its own terms (P6 concern, not touched here).
|
||||
// Hence shared-mcp.json intentionally keeps `"type": "http"` for both cognee
|
||||
// and openclaw-tools rather than switching to `transport`.
|
||||
let SHARED_MCP_SERVERS = {};
|
||||
try {
|
||||
const raw = fs.readFileSync('/shared-mcp.json', 'utf8');
|
||||
SHARED_MCP_SERVERS = JSON.parse(raw).mcpServers || {};
|
||||
} catch (err) {
|
||||
console.error(`shared-mcp.json not loaded (${err.message}); sessions will get no shared MCP servers`);
|
||||
}
|
||||
|
||||
function writeMcpConfig(dir) {
|
||||
const cfg = { mcpServers: SHARED_MCP_SERVERS };
|
||||
|
||||
@@ -169,8 +169,9 @@ services:
|
||||
|
||||
# adolf-llm — conversational Kimi-CLI wrapper (:8010), the model backend for
|
||||
# the Adolf OpenClaw gateway (P2). Real streaming (SSE), chat_id session-keying
|
||||
# + 1:1 kimi resume, media, per-session .mcp.json (shared-MCP servers stubbed
|
||||
# until P4/P5). Needs `kimi login` in adolf-llm-home.
|
||||
# + 1:1 kimi resume, media, per-session .mcp.json sourced from the shared
|
||||
# shared-mcp.json contract (cognee-mcp P4, openclaw-tools P5). Needs
|
||||
# `kimi login` in adolf-llm-home.
|
||||
adolf-llm:
|
||||
build: ./adolf-llm
|
||||
container_name: adolf-llm
|
||||
@@ -179,6 +180,7 @@ services:
|
||||
volumes:
|
||||
- adolf-llm-workspace:/workspace
|
||||
- adolf-llm-home:/root/.kimi-code
|
||||
- ./shared-mcp.json:/shared-mcp.json:ro
|
||||
restart: unless-stopped
|
||||
|
||||
# cognee — Adolf's memory backend (P4). FastAPI + embedded Kuzu graph +
|
||||
@@ -238,6 +240,25 @@ services:
|
||||
depends_on:
|
||||
- cognee
|
||||
|
||||
# openclaw-tools — MCP bridge (P5) exposing a minimal slice of the Adolf
|
||||
# OpenClaw gateway's agent tools (message/cron/nodes/browser) over MCP
|
||||
# Streamable HTTP, so Kimi CLI sessions (adolf-llm) can call them instead of
|
||||
# bypassing OpenClaw entirely. Proxies each MCP tool call to the gateway's
|
||||
# `POST /tools/invoke` HTTP surface (http://adolf:18789). NOTE: `cron` and
|
||||
# `nodes` are hard-denied on that surface by default until P6 adds them to
|
||||
# `gateway.tools.allow` in the adolf openclaw.json — see openclaw-tools/
|
||||
# server.js for the full gate writeup. Not useful until `adolf` (P6) is
|
||||
# configured and running; safe to build/run standalone before that.
|
||||
openclaw-tools:
|
||||
build: ./openclaw-tools
|
||||
container_name: openclaw-tools
|
||||
environment:
|
||||
- OPENCLAW_GATEWAY_URL=http://adolf:18789
|
||||
- OPENCLAW_GATEWAY_TOKEN=${ADOLF_GATEWAY_TOKEN:-}
|
||||
ports:
|
||||
- "8020:8020"
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
kimi-agent-home:
|
||||
adolf-state:
|
||||
|
||||
12
openai/openclaw-tools/Dockerfile
Normal file
12
openai/openclaw-tools/Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM node:22-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json ./
|
||||
RUN npm install --omit=dev
|
||||
|
||||
COPY server.js ./
|
||||
|
||||
EXPOSE 8020
|
||||
|
||||
ENTRYPOINT ["node", "/app/server.js"]
|
||||
11
openai/openclaw-tools/package.json
Normal file
11
openai/openclaw-tools/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "openclaw-tools-bridge",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"description": "MCP bridge exposing a minimal slice of the OpenClaw gateway's agent tools (message/cron/nodes/browser) over Streamable HTTP, for Kimi CLI sessions (adolf-llm) via shared-mcp.json.",
|
||||
"main": "server.js",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"zod": "^4.0.0"
|
||||
}
|
||||
}
|
||||
239
openai/openclaw-tools/server.js
Normal file
239
openai/openclaw-tools/server.js
Normal file
@@ -0,0 +1,239 @@
|
||||
// openclaw-tools bridge (Adolf P5) — a minimal MCP server that proxies a
|
||||
// small slice of the OpenClaw gateway's agent tools (message / cron / nodes /
|
||||
// browser) to Kimi CLI sessions, over MCP Streamable HTTP.
|
||||
//
|
||||
// Why a bridge instead of pointing Kimi straight at the gateway: the gateway
|
||||
// only speaks its own WS/HTTP protocol (`/tools/invoke`, docs/gateway/
|
||||
// tools-invoke-http-api.md in the openclaw source), not MCP. This process is
|
||||
// the thin translation layer: MCP tool call in, `POST {gateway}/tools/invoke`
|
||||
// out, gateway JSON result back as MCP tool content.
|
||||
//
|
||||
// Gate (P6 dependency, verified against the openclaw source docs at
|
||||
// /home/alvis/adolf/docs/gateway/tools-invoke-http-api.md): the gateway's
|
||||
// `/tools/invoke` HTTP surface hard-denies `cron`, `gateway`, and `nodes` by
|
||||
// default, and those three stay owner-only even if `gateway.tools.allow`
|
||||
// re-enables them for non-owner callers. Shared-secret bearer auth (what this
|
||||
// bridge uses) IS treated as a full owner/operator turn, so once P6 adds
|
||||
// `gateway.tools.allow: ["cron", "nodes"]` (or similar) to the running
|
||||
// adolf/openclaw.json, cron_create/cron_list/nodes_invoke below start working
|
||||
// with no change here. `message` and `browser` are NOT in that default deny
|
||||
// list, so message_send should work as soon as the gateway is up and its
|
||||
// normal `tools.*` policy allows those tools for the caller — no special P6
|
||||
// HTTP-deny override needed for those two.
|
||||
//
|
||||
// Until the `adolf` gateway container is actually configured and running
|
||||
// (P6), every proxied call below will fail at the fetch() step (connection
|
||||
// refused) — that is expected for P5 and is NOT a bug in this bridge. What
|
||||
// P5 verifies is the MCP handshake + tool schemas themselves.
|
||||
|
||||
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
|
||||
const { StreamableHTTPServerTransport } = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
||||
const { createMcpExpressApp } = require('@modelcontextprotocol/sdk/server/express.js');
|
||||
const { z } = require('zod');
|
||||
|
||||
const PORT = Number(process.env.PORT) || 8020;
|
||||
const HOST = '0.0.0.0';
|
||||
|
||||
// Gateway base URL: the `adolf` OpenClaw gateway container on the compose
|
||||
// network (docker-compose.yml: ports 18789/18790, service name `adolf`).
|
||||
const GATEWAY_BASE_URL = (process.env.OPENCLAW_GATEWAY_URL || 'http://adolf:18789').replace(/\/+$/, '');
|
||||
// Shared-secret operator token. Accept either env name: OPENCLAW_GATEWAY_TOKEN
|
||||
// is the name the `adolf` service reads OPENCLAW_GATEWAY_TOKEN from internally
|
||||
// (docker-compose.yml sets it from ${ADOLF_GATEWAY_TOKEN:-}), so callers may
|
||||
// reasonably set either var name for this bridge.
|
||||
const GATEWAY_TOKEN = process.env.OPENCLAW_GATEWAY_TOKEN || process.env.ADOLF_GATEWAY_TOKEN || '';
|
||||
const GATEWAY_TIMEOUT_MS = 20_000;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Gateway proxy. POSTs to /tools/invoke (docs/gateway/tools-invoke-http-api.md):
|
||||
// { tool, action, args, sessionKey?, agentId?, idempotencyKey?, dryRun? }
|
||||
// `action` is optional and merges into args.action gateway-side when the
|
||||
// tool schema supports it; we always send it at the top level to match the
|
||||
// documented shape exactly.
|
||||
// Returns MCP tool-result content. Network/HTTP/gateway errors are surfaced
|
||||
// as `isError: true` tool content rather than thrown, so a dead/unconfigured
|
||||
// gateway (expected pre-P6) never breaks the MCP connection itself.
|
||||
async function invokeGatewayTool(tool, action, args) {
|
||||
const body = { tool, args: args || {} };
|
||||
if (action !== undefined) body.action = action;
|
||||
|
||||
let resp;
|
||||
try {
|
||||
resp = await fetch(`${GATEWAY_BASE_URL}/tools/invoke`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(GATEWAY_TOKEN ? { Authorization: `Bearer ${GATEWAY_TOKEN}` } : {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(GATEWAY_TIMEOUT_MS),
|
||||
});
|
||||
} catch (err) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: `openclaw gateway unreachable at ${GATEWAY_BASE_URL} (tool=${tool}): ${err.message || err}`,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
const text = await resp.text();
|
||||
let payload;
|
||||
try { payload = JSON.parse(text); } catch { payload = { raw: text }; }
|
||||
|
||||
if (!resp.ok) {
|
||||
const err = payload && payload.error;
|
||||
const detail = err ? `${err.type || resp.status}: ${err.message || ''}` : `HTTP ${resp.status}`;
|
||||
return {
|
||||
isError: true,
|
||||
content: [{ type: 'text', text: `gateway tool "${tool}" failed (${resp.status}): ${detail}` }],
|
||||
};
|
||||
}
|
||||
|
||||
return { content: [{ type: 'text', text: JSON.stringify(payload.result ?? payload, null, 2) }] };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
function buildServer() {
|
||||
const server = new McpServer({ name: 'openclaw-tools', version: '1.0.0' });
|
||||
|
||||
// --- send-to-channel -------------------------------------------------
|
||||
server.registerTool('message_send', {
|
||||
description:
|
||||
'Send a message to a chat channel through the OpenClaw gateway (Discord, Matrix, Telegram, Slack, WhatsApp, etc). ' +
|
||||
'Proxies to the gateway "message" agent tool, action "send".',
|
||||
inputSchema: {
|
||||
to: z.string().describe(
|
||||
'Delivery target. Format depends on channel: "!room:server" or "@user:server" (Matrix), ' +
|
||||
'"channel:<id>" or "user:<id>" (Discord/Slack), "@username" or chat id (Telegram), E.164 (WhatsApp).',
|
||||
),
|
||||
channel: z.string().optional().describe(
|
||||
'Channel provider id (matrix, telegram, discord, slack, whatsapp, ...). Required if more than one channel is configured.',
|
||||
),
|
||||
message: z.string().optional().describe('Message text.'),
|
||||
media: z.string().optional().describe('Local path or URL of an image/audio/video/document to attach.'),
|
||||
replyTo: z.string().optional().describe('Message id to reply to.'),
|
||||
threadId: z.string().optional().describe('Thread or forum-topic id.'),
|
||||
account: z.string().optional().describe('Account id, when the channel has multiple configured accounts.'),
|
||||
},
|
||||
}, async ({ to, channel, message, media, replyTo, threadId, account }) => {
|
||||
const args = { to };
|
||||
if (channel !== undefined) args.channel = channel;
|
||||
if (message !== undefined) args.message = message;
|
||||
if (media !== undefined) args.media = media;
|
||||
if (replyTo !== undefined) args.replyTo = replyTo;
|
||||
if (threadId !== undefined) args.threadId = threadId;
|
||||
if (account !== undefined) args.account = account;
|
||||
return invokeGatewayTool('message', 'send', args);
|
||||
});
|
||||
|
||||
// --- cron / reminders --------------------------------------------------
|
||||
const CRON_DENY_NOTE =
|
||||
'NOTE: the gateway HTTP /tools/invoke surface hard-denies the "cron" tool by default ' +
|
||||
'(persistent-automation control plane, owner-only) — this call 404s until the gateway operator ' +
|
||||
'adds "cron" to gateway.tools.allow (Adolf P6). Field names mirror `openclaw cron <cmd>` CLI flags ' +
|
||||
'in camelCase and are forwarded near-verbatim; treat gateway 400 error messages as the ground truth ' +
|
||||
'for exact accepted fields.';
|
||||
|
||||
server.registerTool('cron_create', {
|
||||
description: `Schedule a one-shot reminder or recurring job on the OpenClaw gateway cron scheduler. ${CRON_DENY_NOTE}`,
|
||||
inputSchema: {
|
||||
name: z.string().optional().describe('Job name.'),
|
||||
at: z.string().optional().describe('One-shot: ISO 8601 timestamp or relative offset, e.g. "20m".'),
|
||||
every: z.string().optional().describe('Recurring fixed interval, e.g. "10m", "1h", "1d".'),
|
||||
cron: z.string().optional().describe('Recurring 5- or 6-field cron expression.'),
|
||||
tz: z.string().optional().describe('IANA timezone for "at"/"cron" (default: gateway host tz / UTC).'),
|
||||
session: z.enum(['main', 'isolated', 'current']).optional().describe('Execution style (default "main").'),
|
||||
systemEvent: z.string().optional().describe('System-event text payload (no model call).'),
|
||||
message: z.string().optional().describe('Agent-turn prompt payload (model-backed run).'),
|
||||
wake: z.enum(['now', 'next-heartbeat']).optional().describe('Main-session wake mode.'),
|
||||
deleteAfterRun: z.boolean().optional().describe('Auto-delete after a successful one-shot run.'),
|
||||
announce: z.boolean().optional().describe('Deliver the result to a chat channel.'),
|
||||
channel: z.string().optional().describe('Announce delivery channel.'),
|
||||
to: z.string().optional().describe('Announce delivery target.'),
|
||||
},
|
||||
}, async (params) => invokeGatewayTool('cron', 'create', params));
|
||||
|
||||
server.registerTool('cron_list', {
|
||||
description: `List jobs on the OpenClaw gateway cron scheduler. ${CRON_DENY_NOTE}`,
|
||||
inputSchema: {
|
||||
compact: z.boolean().optional().describe('Compact summaries (id, name, enabled, nextRunAtMs, ...). Default true.'),
|
||||
},
|
||||
}, async ({ compact }) => invokeGatewayTool('cron', 'list', { compact: compact ?? true }));
|
||||
|
||||
// --- nodes --------------------------------------------------------------
|
||||
server.registerTool('nodes_invoke', {
|
||||
description:
|
||||
'Invoke a command on a paired OpenClaw node (camera, canvas, location, notify, screen record, etc). ' +
|
||||
'NOTE: the gateway HTTP /tools/invoke surface hard-denies the "nodes" tool by default (node command ' +
|
||||
'relay can reach system.run on paired hosts, owner-only) — this call 404s until the gateway operator ' +
|
||||
'adds "nodes" to gateway.tools.allow (Adolf P6). `system.run`/`system.run.prepare` are blocked on this ' +
|
||||
'path regardless; `system.which` is allowed.',
|
||||
inputSchema: {
|
||||
node: z.string().describe('Node id, display name, or IP.'),
|
||||
command: z.string().describe('Node command, e.g. "canvas.eval", "location.get", "notify", "system.which".'),
|
||||
params: z.record(z.string(), z.unknown()).optional().describe('Command-specific parameters object.'),
|
||||
idempotencyKey: z.string().optional().describe('Optional idempotency key for the invoke.'),
|
||||
},
|
||||
}, async ({ node, command, params, idempotencyKey }) => {
|
||||
const args = { node, command, params: params || {} };
|
||||
if (idempotencyKey !== undefined) args.idempotencyKey = idempotencyKey;
|
||||
return invokeGatewayTool('nodes', 'invoke', args);
|
||||
});
|
||||
|
||||
// --- browser --------------------------------------------------------------
|
||||
server.registerTool('browser_invoke', {
|
||||
description:
|
||||
'Generic pass-through to the OpenClaw gateway "browser" agent tool (agent-controlled Chrome/Brave/Edge ' +
|
||||
'automation: tabs, snapshot, click, type, screenshot). Unlike cron/nodes, "browser" is NOT in the ' +
|
||||
'gateway HTTP default hard-deny list, so this can work as soon as the gateway is up and its normal ' +
|
||||
'tools.* policy allows "browser" for the caller — no special P6 HTTP-deny override needed. Args are ' +
|
||||
'forwarded verbatim as the tool call payload; the exact action/argument vocabulary is defined by the ' +
|
||||
'running gateway and needs live introspection once it exists (deferred to P6/P7).',
|
||||
inputSchema: {
|
||||
action: z.string().describe('Browser tool action, e.g. "status", "open", "snapshot", "click", "type".'),
|
||||
args: z.record(z.string(), z.unknown()).optional().describe('Action-specific parameters object.'),
|
||||
},
|
||||
}, async ({ action, args }) => invokeGatewayTool('browser', action, args || {}));
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stateless Streamable HTTP transport (mirrors the MCP SDK's own
|
||||
// examples/server/simpleStatelessStreamableHttp.js): one McpServer + one
|
||||
// transport per request, no session persistence needed for these tools.
|
||||
const app = createMcpExpressApp({ host: HOST });
|
||||
|
||||
app.get('/health', (_req, res) => res.status(200).json({ ok: true }));
|
||||
|
||||
app.post('/mcp', async (req, res) => {
|
||||
const server = buildServer();
|
||||
try {
|
||||
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
res.on('close', () => {
|
||||
transport.close();
|
||||
server.close();
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('error handling MCP request:', err);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'internal server error' }, id: null });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/mcp', (_req, res) => {
|
||||
res.writeHead(405).end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'method not allowed' }, id: null }));
|
||||
});
|
||||
|
||||
app.delete('/mcp', (_req, res) => {
|
||||
res.writeHead(405).end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32000, message: 'method not allowed' }, id: null }));
|
||||
});
|
||||
|
||||
app.listen(PORT, HOST, () => {
|
||||
console.log(`openclaw-tools bridge listening on ${HOST}:${PORT} (gateway: ${GATEWAY_BASE_URL})`);
|
||||
});
|
||||
@@ -3,6 +3,10 @@
|
||||
"cognee": {
|
||||
"type": "http",
|
||||
"url": "http://cognee-mcp:8000/mcp"
|
||||
},
|
||||
"openclaw-tools": {
|
||||
"type": "http",
|
||||
"url": "http://openclaw-tools:8020/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user