mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:30:44 +00:00
fix: lazy-start gateway mcp loopback
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { ensureMcpLoopbackServer } from "../../gateway/mcp-http.js";
|
||||
import {
|
||||
createMcpLoopbackServerConfig,
|
||||
getActiveMcpLoopbackRuntime,
|
||||
@@ -36,6 +37,7 @@ const prepareDeps = {
|
||||
makeBootstrapWarn: makeBootstrapWarnImpl,
|
||||
resolveBootstrapContextForRun: resolveBootstrapContextForRunImpl,
|
||||
getActiveMcpLoopbackRuntime,
|
||||
ensureMcpLoopbackServer,
|
||||
createMcpLoopbackServerConfig,
|
||||
resolveOpenClawDocsPath: async (
|
||||
params: Parameters<typeof import("../docs-path.js").resolveOpenClawDocsPath>[0],
|
||||
@@ -114,9 +116,17 @@ export async function prepareCliRunContext(
|
||||
config: params.config,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
const mcpLoopbackRuntime = backendResolved.bundleMcp
|
||||
let mcpLoopbackRuntime = backendResolved.bundleMcp
|
||||
? prepareDeps.getActiveMcpLoopbackRuntime()
|
||||
: undefined;
|
||||
if (backendResolved.bundleMcp && !mcpLoopbackRuntime) {
|
||||
try {
|
||||
await prepareDeps.ensureMcpLoopbackServer();
|
||||
} catch (error) {
|
||||
cliBackendLog.warn(`mcp loopback server failed to start: ${String(error)}`);
|
||||
}
|
||||
mcpLoopbackRuntime = prepareDeps.getActiveMcpLoopbackRuntime();
|
||||
}
|
||||
const preparedBackend = await prepareCliBundleMcpConfig({
|
||||
enabled: backendResolved.bundleMcp,
|
||||
mode: backendResolved.bundleMcpMode,
|
||||
|
||||
@@ -32,7 +32,9 @@ vi.mock("./tool-resolution.js", () => ({
|
||||
|
||||
import {
|
||||
createMcpLoopbackServerConfig,
|
||||
closeMcpLoopbackServer,
|
||||
getActiveMcpLoopbackRuntime,
|
||||
ensureMcpLoopbackServer,
|
||||
startMcpLoopbackServer,
|
||||
} from "./mcp-http.js";
|
||||
|
||||
@@ -162,6 +164,19 @@ describe("mcp loopback server", () => {
|
||||
expect(getActiveMcpLoopbackRuntime()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("starts the loopback server lazily and reuses the same singleton", async () => {
|
||||
expect(getActiveMcpLoopbackRuntime()).toBeUndefined();
|
||||
|
||||
const first = await ensureMcpLoopbackServer(0);
|
||||
const second = await ensureMcpLoopbackServer(0);
|
||||
|
||||
expect(second).toBe(first);
|
||||
expect(getActiveMcpLoopbackRuntime()?.port).toBe(first.port);
|
||||
|
||||
await closeMcpLoopbackServer();
|
||||
expect(getActiveMcpLoopbackRuntime()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns 401 when the bearer token is missing", async () => {
|
||||
server = await startMcpLoopbackServer(0);
|
||||
const response = await sendRaw({
|
||||
|
||||
@@ -23,6 +23,14 @@ export {
|
||||
getActiveMcpLoopbackRuntime,
|
||||
} from "./mcp-http.loopback-runtime.js";
|
||||
|
||||
type McpLoopbackServer = {
|
||||
port: number;
|
||||
close: () => Promise<void>;
|
||||
};
|
||||
|
||||
let activeMcpLoopbackServer: McpLoopbackServer | undefined;
|
||||
let activeMcpLoopbackServerPromise: Promise<McpLoopbackServer> | null = null;
|
||||
|
||||
export async function startMcpLoopbackServer(port = 0): Promise<{
|
||||
port: number;
|
||||
close: () => Promise<void>;
|
||||
@@ -98,13 +106,16 @@ export async function startMcpLoopbackServer(port = 0): Promise<{
|
||||
setActiveMcpLoopbackRuntime({ port: address.port, token });
|
||||
logDebug(`mcp loopback listening on 127.0.0.1:${address.port}`);
|
||||
|
||||
return {
|
||||
const server: McpLoopbackServer = {
|
||||
port: address.port,
|
||||
close: () =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
httpServer.close((error) => {
|
||||
if (!error) {
|
||||
clearActiveMcpLoopbackRuntime(token);
|
||||
if (activeMcpLoopbackServer === server) {
|
||||
activeMcpLoopbackServer = undefined;
|
||||
}
|
||||
}
|
||||
if (error) {
|
||||
reject(error);
|
||||
@@ -114,4 +125,33 @@ export async function startMcpLoopbackServer(port = 0): Promise<{
|
||||
});
|
||||
}),
|
||||
};
|
||||
return server;
|
||||
}
|
||||
|
||||
export async function ensureMcpLoopbackServer(port = 0): Promise<McpLoopbackServer> {
|
||||
if (activeMcpLoopbackServer) {
|
||||
return activeMcpLoopbackServer;
|
||||
}
|
||||
if (!activeMcpLoopbackServerPromise) {
|
||||
activeMcpLoopbackServerPromise = startMcpLoopbackServer(port)
|
||||
.then((server) => {
|
||||
activeMcpLoopbackServer = server;
|
||||
return server;
|
||||
})
|
||||
.finally(() => {
|
||||
activeMcpLoopbackServerPromise = null;
|
||||
});
|
||||
}
|
||||
return activeMcpLoopbackServerPromise;
|
||||
}
|
||||
|
||||
export async function closeMcpLoopbackServer(): Promise<void> {
|
||||
const server =
|
||||
activeMcpLoopbackServer ??
|
||||
(activeMcpLoopbackServerPromise ? await activeMcpLoopbackServerPromise : undefined);
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
activeMcpLoopbackServer = undefined;
|
||||
await server.close();
|
||||
}
|
||||
|
||||
44
src/gateway/server-startup-early.test.ts
Normal file
44
src/gateway/server-startup-early.test.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { startGatewayEarlyRuntime } from "./server-startup-early.js";
|
||||
|
||||
describe("startGatewayEarlyRuntime", () => {
|
||||
it("does not eagerly start the MCP loopback server", async () => {
|
||||
const earlyRuntime = await startGatewayEarlyRuntime({
|
||||
minimalTestGateway: true,
|
||||
cfgAtStart: {} as never,
|
||||
port: 18_789,
|
||||
gatewayTls: { enabled: false },
|
||||
tailscaleMode: "off" as never,
|
||||
log: {
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
},
|
||||
logDiscovery: {
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
},
|
||||
nodeRegistry: {} as never,
|
||||
broadcast: () => {},
|
||||
nodeSendToAllSubscribed: () => {},
|
||||
getPresenceVersion: () => 0,
|
||||
getHealthVersion: () => 0,
|
||||
refreshGatewayHealthSnapshot: () => {},
|
||||
logHealth: () => {},
|
||||
dedupe: () => {},
|
||||
chatAbortControllers: new Map(),
|
||||
chatRunState: new Map(),
|
||||
chatRunBuffers: new Map(),
|
||||
chatDeltaSentAt: new Map(),
|
||||
chatDeltaLastBroadcastLen: new Map(),
|
||||
removeChatRun: () => {},
|
||||
agentRunSeq: () => 0,
|
||||
nodeSendToSession: () => {},
|
||||
skillsRefreshDelayMs: 30_000,
|
||||
getSkillsRefreshTimer: () => null,
|
||||
setSkillsRefreshTimer: () => {},
|
||||
loadConfig: () => ({}) as never,
|
||||
});
|
||||
|
||||
expect(earlyRuntime).not.toHaveProperty("mcpServer");
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
setSkillsRemoteRegistry,
|
||||
} from "../infra/skills-remote.js";
|
||||
import { startTaskRegistryMaintenance } from "../tasks/task-registry.maintenance.js";
|
||||
import { startMcpLoopbackServer } from "./mcp-http.js";
|
||||
import { startGatewayDiscovery } from "./server-discovery-runtime.js";
|
||||
import { startGatewayMaintenanceTimers } from "./server-maintenance.js";
|
||||
|
||||
@@ -54,14 +53,6 @@ export async function startGatewayEarlyRuntime(params: {
|
||||
setSkillsRefreshTimer: (timer: ReturnType<typeof setTimeout> | null) => void;
|
||||
loadConfig: () => OpenClawConfig;
|
||||
}) {
|
||||
let mcpServer: { port: number; close: () => Promise<void> } | undefined;
|
||||
try {
|
||||
mcpServer = await startMcpLoopbackServer(0);
|
||||
params.log.info(`MCP loopback server listening on http://127.0.0.1:${mcpServer.port}/mcp`);
|
||||
} catch (error) {
|
||||
params.log.warn(`MCP loopback server failed to start: ${String(error)}`);
|
||||
}
|
||||
|
||||
let bonjourStop: (() => Promise<void>) | null = null;
|
||||
if (!params.minimalTestGateway) {
|
||||
const machineDisplayName = await getMachineDisplayName();
|
||||
@@ -127,7 +118,6 @@ export async function startGatewayEarlyRuntime(params: {
|
||||
});
|
||||
|
||||
return {
|
||||
mcpServer,
|
||||
bonjourStop,
|
||||
skillsChangeUnsub,
|
||||
maintenance,
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
import { runSetupWizard } from "../wizard/setup.js";
|
||||
import { createAuthRateLimiter, type AuthRateLimiter } from "./auth-rate-limit.js";
|
||||
import { resolveGatewayAuth } from "./auth.js";
|
||||
import { closeMcpLoopbackServer } from "./mcp-http.js";
|
||||
import { createGatewayAuxHandlers } from "./server-aux-handlers.js";
|
||||
import { createChannelManager } from "./server-channels.js";
|
||||
import { createGatewayCloseHandler, runGatewayClosePrelude } from "./server-close.js";
|
||||
@@ -502,7 +503,7 @@ export async function startGatewayServer(
|
||||
stopModelPricingRefresh: runtimeState.stopModelPricingRefresh,
|
||||
stopChannelHealthMonitor: () => runtimeState?.channelHealthMonitor?.stop(),
|
||||
clearSecretsRuntimeSnapshot,
|
||||
closeMcpServer: async () => await runtimeState?.mcpServer?.close(),
|
||||
closeMcpServer: async () => await closeMcpLoopbackServer(),
|
||||
});
|
||||
const closeOnStartupFailure = async () => {
|
||||
await runClosePrelude();
|
||||
@@ -574,7 +575,6 @@ export async function startGatewayServer(
|
||||
},
|
||||
loadConfig,
|
||||
});
|
||||
runtimeState.mcpServer = earlyRuntime.mcpServer;
|
||||
runtimeState.bonjourStop = earlyRuntime.bonjourStop;
|
||||
runtimeState.skillsChangeUnsub = earlyRuntime.skillsChangeUnsub;
|
||||
if (earlyRuntime.maintenance) {
|
||||
|
||||
Reference in New Issue
Block a user