mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
60bd646478
Add an explicit Streamable HTTP entry for environment/gateway hosts while keeping stdio registrations and default behavior. Preserve the shared provider/session owner across DELETE and replacement; fence and drain tool/completion work before handover or teardown. Bound HTTP input and plain-response admission, cancel refused bodies, join whole exchanges and asynchronous response cleanup before provider hooks, gate startup readiness, and reject queued HTTP/1.1 pipelining while retaining sequential keep-alive. Validation: 8 static/build tasks, 83 adapter tests and registry metadata pass. A real installed gh binary produces seven equivalent CLI/stdio/HTTP reports with DELETE/replacement and clean signal shutdown. Broader local native-provider/ownership failures reproduce on clean main and are documented with limitations in the PR. Co-authored-by: Edgar Arakelyan <edgar.arakelyan@scale.com> Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
93 lines
3.1 KiB
TypeScript
93 lines
3.1 KiB
TypeScript
import {
|
|
McpServer,
|
|
type Icon,
|
|
type RegisteredTool,
|
|
type ServerContext,
|
|
type ScopeChallengeHandler,
|
|
type StandardSchemaWithJSON,
|
|
type ToolAnnotations,
|
|
type ToolCallback,
|
|
} from "@modelcontextprotocol/server";
|
|
|
|
/** Tracks tools automatically; other resource-using MCP callbacks use runRequest. */
|
|
export class RequestTrackingMcpServer extends McpServer {
|
|
readonly #calls = new Set<Promise<unknown>>();
|
|
#tracking = false;
|
|
|
|
/** HTTP sessions share resources across transports; stdio keeps the SDK's original behavior. */
|
|
enableRequestTracking(): void {
|
|
this.#tracking = true;
|
|
}
|
|
|
|
override registerTool<
|
|
OutputArgs extends StandardSchemaWithJSON,
|
|
InputArgs extends StandardSchemaWithJSON | undefined = undefined,
|
|
>(
|
|
name: string,
|
|
config: {
|
|
title?: string;
|
|
description?: string;
|
|
inputSchema?: InputArgs;
|
|
outputSchema?: OutputArgs;
|
|
annotations?: ToolAnnotations;
|
|
icons?: Icon[];
|
|
scopeChallenge?: ScopeChallengeHandler;
|
|
_meta?: Record<string, unknown>;
|
|
},
|
|
callback: ToolCallback<InputArgs>,
|
|
): RegisteredTool {
|
|
// The SDK calls a schema-less callback with context alone, and a schema'd
|
|
// callback with (validated input, context). Narrow its conditional type at
|
|
// the same inputSchema discriminant used by the SDK.
|
|
const tracked =
|
|
config.inputSchema === undefined
|
|
? (context: ServerContext) =>
|
|
this.runRequest(context.mcpReq.signal, () =>
|
|
(callback as ToolCallback<undefined>)(context),
|
|
)
|
|
: (args: unknown, context: ServerContext) =>
|
|
this.runRequest(context.mcpReq.signal, () =>
|
|
(callback as ToolCallback<StandardSchemaWithJSON>)(args, context),
|
|
);
|
|
return super.registerTool<OutputArgs, InputArgs>(
|
|
name,
|
|
config,
|
|
tracked as ToolCallback<InputArgs>,
|
|
);
|
|
}
|
|
|
|
/** Retain HTTP dispatch, tool and completion work under the HTTP owner's shared drain. */
|
|
runRequest<T>(
|
|
signal: AbortSignal | undefined,
|
|
callback: () => T | Promise<T>,
|
|
): T | Promise<T> {
|
|
if (!this.#tracking) return callback();
|
|
// SDK validation can finish after its transport closed. Such a callback
|
|
// must never start work against the next session's shared resources.
|
|
const call = Promise.resolve().then(() => {
|
|
signal?.throwIfAborted();
|
|
return callback();
|
|
});
|
|
this.#calls.add(call);
|
|
return call.finally(() => this.#calls.delete(call));
|
|
}
|
|
|
|
/** Wait for running handlers, including their finally blocks; a timeout retains ownership. */
|
|
async drainRequests(timeoutMs?: number): Promise<boolean> {
|
|
if (this.#calls.size === 0) return true;
|
|
const settled = Promise.allSettled(this.#calls).then(() => true);
|
|
if (timeoutMs === undefined) return settled;
|
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
try {
|
|
return await Promise.race([
|
|
settled,
|
|
new Promise<false>((resolve) => {
|
|
timer = setTimeout(() => resolve(false), timeoutMs);
|
|
}),
|
|
]);
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
}
|