mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-03 16:30:23 +00:00
108 lines
3.1 KiB
TypeScript
108 lines
3.1 KiB
TypeScript
type SseMcpServerLaunchConfig = {
|
|
url: string;
|
|
headers?: Record<string, string>;
|
|
};
|
|
|
|
type SseMcpServerLaunchResult =
|
|
| { ok: true; config: SseMcpServerLaunchConfig }
|
|
| { ok: false; reason: string };
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
}
|
|
|
|
function toStringRecord(
|
|
value: unknown,
|
|
warnDropped?: (key: string, entry: unknown) => void,
|
|
): Record<string, string> | undefined {
|
|
if (!isRecord(value)) {
|
|
return undefined;
|
|
}
|
|
const entries = Object.entries(value)
|
|
.map(([key, entry]) => {
|
|
if (typeof entry === "string") {
|
|
return [key, entry] as const;
|
|
}
|
|
if (typeof entry === "number" || typeof entry === "boolean") {
|
|
return [key, String(entry)] as const;
|
|
}
|
|
warnDropped?.(key, entry);
|
|
return null;
|
|
})
|
|
.filter((entry): entry is readonly [string, string] => entry !== null);
|
|
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
}
|
|
|
|
export function resolveSseMcpServerLaunchConfig(
|
|
raw: unknown,
|
|
options?: {
|
|
onDroppedHeader?: (key: string, value: unknown) => void;
|
|
onMalformedHeaders?: (value: unknown) => void;
|
|
},
|
|
): SseMcpServerLaunchResult {
|
|
if (!isRecord(raw)) {
|
|
return { ok: false, reason: "server config must be an object" };
|
|
}
|
|
if (typeof raw.url !== "string" || raw.url.trim().length === 0) {
|
|
return { ok: false, reason: "its url is missing" };
|
|
}
|
|
const url = raw.url.trim();
|
|
let parsed: URL;
|
|
try {
|
|
parsed = new URL(url);
|
|
} catch {
|
|
return { ok: false, reason: `its url is not a valid URL: ${url}` };
|
|
}
|
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
return {
|
|
ok: false,
|
|
reason: `only http and https URLs are supported, got ${parsed.protocol}`,
|
|
};
|
|
}
|
|
// Warn if headers is present but not an object (e.g. a string or array).
|
|
let headers: Record<string, string> | undefined;
|
|
if (raw.headers !== undefined && raw.headers !== null) {
|
|
if (!isRecord(raw.headers)) {
|
|
options?.onMalformedHeaders?.(raw.headers);
|
|
} else {
|
|
headers = toStringRecord(raw.headers, options?.onDroppedHeader);
|
|
}
|
|
}
|
|
return {
|
|
ok: true,
|
|
config: {
|
|
url,
|
|
headers,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function describeSseMcpServerLaunchConfig(config: SseMcpServerLaunchConfig): string {
|
|
try {
|
|
const parsed = new URL(config.url);
|
|
// Redact embedded credentials and query-token auth from log/description output.
|
|
if (parsed.username || parsed.password) {
|
|
parsed.username = parsed.username ? "***" : "";
|
|
parsed.password = parsed.password ? "***" : "";
|
|
}
|
|
for (const key of parsed.searchParams.keys()) {
|
|
const lower = key.toLowerCase();
|
|
if (
|
|
lower === "token" ||
|
|
lower === "key" ||
|
|
lower === "api_key" ||
|
|
lower === "apikey" ||
|
|
lower === "secret" ||
|
|
lower === "access_token"
|
|
) {
|
|
parsed.searchParams.set(key, "***");
|
|
}
|
|
}
|
|
return parsed.toString();
|
|
} catch {
|
|
return config.url;
|
|
}
|
|
}
|
|
|
|
export type { SseMcpServerLaunchConfig, SseMcpServerLaunchResult };
|