mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 10:43:30 +00:00
rea mcp --http printed "Choose another --port or stop the process using it" for every listener failure except a missing host allowlist, so a host lookup failure, an unassigned address or a permission denial got port advice. StreamableHttpListenError already classifies the native failure and owns a recovery for each kind. Startup now always prints that owned recovery behind one shared lead-in, and the fixed message is removed. The native cause text stays out of stderr as before. Fixes #7346 Co-Authored-By: gensokyo-dev <gensokyo-dev@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
245 lines
8.5 KiB
TypeScript
245 lines
8.5 KiB
TypeScript
import type { AnalysisSettings } from "../application/AnalysisSettings.js";
|
|
import { createAndroidAnalysisProvider } from "../composition/android.js";
|
|
import type { AndroidAnalysisPort } from "../application/android/AndroidAnalysisPort.js";
|
|
|
|
import type { BinarySession } from "../application/binary/BinarySession.js";
|
|
import type { Logger } from "pino";
|
|
import { createServer } from "../server/createServer.js";
|
|
import { StreamableHttpListenError } from "../server/http/streamableHttpListenError.js";
|
|
import type { ToolResultDelivery } from "../server/ToolResultDelivery.js";
|
|
import type { McpServerHandle, RuntimeDependencies } from "./types.js";
|
|
import type { OptionalProviderLoadResult } from "../application/OptionalObservationProviders.js";
|
|
import { OwnedMcpServers } from "./OwnedMcpServers.js";
|
|
import {
|
|
MCP_CONNECTION_START_FAILED,
|
|
mcpHttpListenFailed,
|
|
} from "./messages.js";
|
|
import { reportMcpTransportError } from "./reportMcpTransportError.js";
|
|
|
|
interface ServerContext {
|
|
readonly analysisSettings?: AnalysisSettings;
|
|
readonly environment: Readonly<NodeJS.ProcessEnv>;
|
|
readonly delivery: ToolResultDelivery;
|
|
readonly logger: Logger;
|
|
readonly serverLogger: Logger;
|
|
readonly loadOptionalProviders: () => Promise<OptionalProviderLoadResult>;
|
|
readonly signal?: AbortSignal;
|
|
}
|
|
|
|
type ClosableOptionalProvider = {
|
|
readonly close?: () => Promise<void>;
|
|
};
|
|
|
|
/** Start the injected transport (stdio or streamable HTTP) around one shared session. */
|
|
export const startMcpTransport = async (
|
|
dependencies: RuntimeDependencies,
|
|
session: BinarySession,
|
|
serverContext: ServerContext,
|
|
): Promise<
|
|
| {
|
|
readonly ok: true;
|
|
readonly handle: McpServerHandle;
|
|
readonly closeAndroid: () => Promise<void>;
|
|
readonly closeOptionalProviders: () => Promise<void>;
|
|
}
|
|
| {
|
|
readonly ok: false;
|
|
readonly cancelled: boolean;
|
|
readonly handle: McpServerHandle;
|
|
readonly closeAndroid: () => Promise<void>;
|
|
readonly closeOptionalProviders: () => Promise<void>;
|
|
}
|
|
> => {
|
|
const { serverLogger } = serverContext;
|
|
let optionalProviders: OptionalProviderLoadResult = {};
|
|
try {
|
|
optionalProviders = await serverContext.loadOptionalProviders();
|
|
} catch (cause: unknown) {
|
|
serverLogger.warn(
|
|
{
|
|
error: cause instanceof Error ? cause.message : String(cause),
|
|
},
|
|
"Optional MCP providers could not load; affected tools remain unavailable",
|
|
);
|
|
}
|
|
const { optionalProviderLoadFailures, ...observationProviders } =
|
|
optionalProviders;
|
|
for (const failure of Object.values(optionalProviderLoadFailures ?? {})) {
|
|
serverLogger.warn(
|
|
{ providerId: failure.providerId, error: failure.reason },
|
|
"Optional MCP adapter could not load; its peers remain available",
|
|
);
|
|
}
|
|
const androidProviders = new Set<AndroidAnalysisPort>();
|
|
const servers = new OwnedMcpServers<ReturnType<typeof createServer>>();
|
|
const closeServers = async (): Promise<void> => {
|
|
const results = await servers.closeAll();
|
|
const failures = results.flatMap((result) =>
|
|
result.status === "rejected" ? [result.reason] : [],
|
|
);
|
|
if (failures.length > 0)
|
|
throw new AggregateError(failures, "REA server cleanup failed");
|
|
};
|
|
const closeAndroid = async (): Promise<void> => {
|
|
const results = await Promise.allSettled(
|
|
[...androidProviders].map(async (provider) => {
|
|
await provider.close();
|
|
androidProviders.delete(provider);
|
|
}),
|
|
);
|
|
const failures = results.flatMap((result) =>
|
|
result.status === "rejected" ? [result.reason] : [],
|
|
);
|
|
if (failures.length === 1) throw failures[0];
|
|
if (failures.length > 1)
|
|
throw new AggregateError(failures, "Android provider cleanup failed");
|
|
};
|
|
// Optional providers are shared by every server instance the SDK creates,
|
|
// including a discarded discovery probe, so only process shutdown closes them.
|
|
const optionalProvidersToClose = new Set<ClosableOptionalProvider>(
|
|
Object.values(observationProviders).filter(
|
|
(provider) => provider !== undefined,
|
|
),
|
|
);
|
|
let optionalProviderClose: Promise<void> | undefined;
|
|
const closeOptionalProviders = async (): Promise<void> => {
|
|
optionalProviderClose ??= (async () => {
|
|
const results = await Promise.allSettled(
|
|
[...optionalProvidersToClose].map(async (provider) => {
|
|
await provider.close?.();
|
|
optionalProvidersToClose.delete(provider);
|
|
}),
|
|
);
|
|
const failures = results.flatMap((result) =>
|
|
result.status === "rejected" ? [result.reason] : [],
|
|
);
|
|
if (failures.length > 0)
|
|
throw new AggregateError(failures, "Optional provider cleanup failed");
|
|
})().catch((cause: unknown) => {
|
|
optionalProviderClose = undefined;
|
|
throw cause;
|
|
});
|
|
await optionalProviderClose;
|
|
};
|
|
let handle: McpServerHandle;
|
|
try {
|
|
if (serverContext.signal?.aborted) throw serverContext.signal.reason;
|
|
handle = await dependencies.serve(
|
|
() =>
|
|
servers.create(() => {
|
|
// The SDK can discard a discovery probe and construct a replacement server.
|
|
const android = createAndroidAnalysisProvider(
|
|
serverContext.environment,
|
|
undefined,
|
|
serverContext.analysisSettings?.read,
|
|
);
|
|
androidProviders.add(android);
|
|
const closeAndroidProvider = android.close.bind(android);
|
|
android.close = async () => {
|
|
await closeAndroidProvider();
|
|
androidProviders.delete(android);
|
|
};
|
|
const server = (dependencies.createServer ?? createServer)(
|
|
{ kind: "session", session },
|
|
{
|
|
...(serverContext.analysisSettings === undefined
|
|
? {}
|
|
: { analysisSettings: serverContext.analysisSettings }),
|
|
logger: serverContext.logger,
|
|
environment: serverContext.environment,
|
|
delivery: serverContext.delivery,
|
|
...optionalProviders,
|
|
optionalObservationProviderOwner: "caller",
|
|
androidAnalysis: android,
|
|
},
|
|
);
|
|
return server;
|
|
}),
|
|
{
|
|
onerror: (error) =>
|
|
reportMcpTransportError(
|
|
error,
|
|
serverLogger,
|
|
dependencies.writeStderr,
|
|
),
|
|
logger: serverLogger,
|
|
...(serverContext.signal === undefined
|
|
? {}
|
|
: { signal: serverContext.signal }),
|
|
},
|
|
);
|
|
} catch (cause: unknown) {
|
|
const cancelled =
|
|
serverContext.signal?.aborted === true &&
|
|
cause === serverContext.signal.reason;
|
|
const [androidCleanup, serverCleanup, optionalProviderCleanup] =
|
|
await Promise.allSettled([
|
|
closeAndroid(),
|
|
closeServers(),
|
|
closeOptionalProviders(),
|
|
]);
|
|
if (!cancelled) {
|
|
const startFailure =
|
|
cause instanceof StreamableHttpListenError
|
|
? mcpHttpListenFailed(cause.remediationAction)
|
|
: MCP_CONNECTION_START_FAILED;
|
|
serverLogger.error(
|
|
{
|
|
error: cause instanceof Error ? cause.message : String(cause),
|
|
},
|
|
startFailure,
|
|
);
|
|
dependencies.writeStderr(`${startFailure}\n`);
|
|
}
|
|
for (const cleanup of [
|
|
androidCleanup,
|
|
serverCleanup,
|
|
optionalProviderCleanup,
|
|
]) {
|
|
if (cleanup.status === "rejected") {
|
|
const reason =
|
|
cleanup.reason instanceof Error
|
|
? cleanup.reason.message
|
|
: String(cleanup.reason);
|
|
serverLogger.error(
|
|
{ cleanup_error: reason },
|
|
"Provider cleanup rejected after MCP startup",
|
|
);
|
|
dependencies.writeStderr(`Provider cleanup failed: ${reason}\n`);
|
|
}
|
|
}
|
|
// Return the same partial owners for retry; the caller owns the binary
|
|
// session independently of transport admission.
|
|
return {
|
|
ok: false,
|
|
cancelled,
|
|
handle: { close: closeServers },
|
|
closeAndroid,
|
|
closeOptionalProviders,
|
|
};
|
|
}
|
|
return {
|
|
ok: true,
|
|
handle: {
|
|
...(handle.prepareShutdown === undefined
|
|
? {}
|
|
: { prepareShutdown: handle.prepareShutdown }),
|
|
close: async () => {
|
|
await handle.prepareShutdown?.();
|
|
const results = await Promise.allSettled([
|
|
handle.close(),
|
|
closeServers(),
|
|
]);
|
|
const failures = results.flatMap((result) =>
|
|
result.status === "rejected" ? [result.reason] : [],
|
|
);
|
|
if (failures.length === 1) throw failures[0];
|
|
if (failures.length > 1)
|
|
throw new AggregateError(failures, "REA transport cleanup failed");
|
|
},
|
|
},
|
|
closeAndroid,
|
|
closeOptionalProviders,
|
|
};
|
|
};
|