mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-11 01:01:13 +00:00
101 lines
3.1 KiB
TypeScript
101 lines
3.1 KiB
TypeScript
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
import { loadConfig } from "../config/config.js";
|
|
import { resolveMainSessionKey } from "../config/sessions.js";
|
|
import { normalizeMessageChannel } from "../utils/message-channel.js";
|
|
import { getHeader } from "./http-utils.js";
|
|
|
|
const MAX_MCP_BODY_BYTES = 1_048_576;
|
|
|
|
export type McpRequestContext = {
|
|
sessionKey: string;
|
|
messageProvider: string | undefined;
|
|
accountId: string | undefined;
|
|
};
|
|
|
|
function resolveScopedSessionKey(
|
|
cfg: ReturnType<typeof loadConfig>,
|
|
rawSessionKey: string | undefined,
|
|
): string {
|
|
const trimmed = rawSessionKey?.trim();
|
|
return !trimmed || trimmed === "main" ? resolveMainSessionKey(cfg) : trimmed;
|
|
}
|
|
|
|
export function validateMcpLoopbackRequest(params: {
|
|
req: IncomingMessage;
|
|
res: ServerResponse;
|
|
token: string;
|
|
}): boolean {
|
|
let url: URL;
|
|
try {
|
|
url = new URL(params.req.url ?? "/", `http://${params.req.headers.host ?? "localhost"}`);
|
|
} catch {
|
|
params.res.writeHead(400, { "Content-Type": "application/json" });
|
|
params.res.end(JSON.stringify({ error: "bad_request" }));
|
|
return false;
|
|
}
|
|
|
|
if (params.req.method === "GET" && url.pathname.startsWith("/.well-known/")) {
|
|
params.res.writeHead(404);
|
|
params.res.end();
|
|
return false;
|
|
}
|
|
|
|
if (url.pathname !== "/mcp") {
|
|
params.res.writeHead(404, { "Content-Type": "application/json" });
|
|
params.res.end(JSON.stringify({ error: "not_found" }));
|
|
return false;
|
|
}
|
|
|
|
if (params.req.method !== "POST") {
|
|
params.res.writeHead(405, { Allow: "POST" });
|
|
params.res.end();
|
|
return false;
|
|
}
|
|
|
|
const authHeader = getHeader(params.req, "authorization") ?? "";
|
|
if (authHeader !== `Bearer ${params.token}`) {
|
|
params.res.writeHead(401, { "Content-Type": "application/json" });
|
|
params.res.end(JSON.stringify({ error: "unauthorized" }));
|
|
return false;
|
|
}
|
|
|
|
const contentType = getHeader(params.req, "content-type") ?? "";
|
|
if (!contentType.startsWith("application/json")) {
|
|
params.res.writeHead(415, { "Content-Type": "application/json" });
|
|
params.res.end(JSON.stringify({ error: "unsupported_media_type" }));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
export async function readMcpHttpBody(req: IncomingMessage): Promise<string> {
|
|
return await new Promise((resolve, reject) => {
|
|
const chunks: Buffer[] = [];
|
|
let received = 0;
|
|
req.on("data", (chunk: Buffer) => {
|
|
received += chunk.length;
|
|
if (received > MAX_MCP_BODY_BYTES) {
|
|
req.destroy();
|
|
reject(new Error(`Request body exceeds ${MAX_MCP_BODY_BYTES} bytes`));
|
|
return;
|
|
}
|
|
chunks.push(chunk);
|
|
});
|
|
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
req.on("error", reject);
|
|
});
|
|
}
|
|
|
|
export function resolveMcpRequestContext(
|
|
req: IncomingMessage,
|
|
cfg: ReturnType<typeof loadConfig>,
|
|
): McpRequestContext {
|
|
return {
|
|
sessionKey: resolveScopedSessionKey(cfg, getHeader(req, "x-session-key")),
|
|
messageProvider:
|
|
normalizeMessageChannel(getHeader(req, "x-openclaw-message-channel")) ?? undefined,
|
|
accountId: getHeader(req, "x-openclaw-account-id")?.trim() || undefined,
|
|
};
|
|
}
|