Merge remote-tracking branch 'origin/v2/main' into v2/feat/2294-skills-catalog-budget-settings
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
commit
621b712229
6 changed files with 3427 additions and 63 deletions
|
|
@ -58,6 +58,7 @@ export function createRemoteTransport(
|
|||
onStderr: transportOptions.onStderr,
|
||||
onFetchRequest: transportOptions.onFetchRequest,
|
||||
onFetchResponseBody: transportOptions.onFetchResponseBody,
|
||||
onFetchStreamUpdate: transportOptions.onFetchStreamUpdate,
|
||||
authProvider: transportOptions.authProvider,
|
||||
settings: transportOptions.settings,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
import type { Transport } from "@modelcontextprotocol/client";
|
||||
import type { JSONRPCMessage } from "@modelcontextprotocol/client";
|
||||
import type { FetchRequestEntryBase } from "../../types.js";
|
||||
import type { FetchRequestEntryBase, FetchStreamState } from "../../types.js";
|
||||
import type { RemoteEvent } from "../types.js";
|
||||
import type { AuthChallenge } from "../../../auth/challenge.js";
|
||||
import { AuthChallengeError } from "../../../auth/challenge.js";
|
||||
|
|
@ -328,6 +328,17 @@ export class RemoteSession {
|
|||
});
|
||||
}
|
||||
|
||||
onFetchStreamUpdate(id: string, stream: FetchStreamState): void {
|
||||
this.pushEvent({
|
||||
type: "fetch_stream_update",
|
||||
data: {
|
||||
id,
|
||||
eventCount: stream.eventCount,
|
||||
...(stream.closedAt && { closedAt: stream.closedAt.toISOString() }),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onStderr(entry: { timestamp: Date; message: string }): void {
|
||||
this.pushEvent({
|
||||
type: "stdio_log",
|
||||
|
|
|
|||
|
|
@ -869,6 +869,8 @@ export function createRemoteApp(
|
|||
onFetchRequest: (entry) => session.onFetchRequest(entry),
|
||||
onFetchResponseBody: (id, body) =>
|
||||
session.onFetchResponseBody(id, body),
|
||||
onFetchStreamUpdate: (id, stream) =>
|
||||
session.onFetchStreamUpdate(id, stream),
|
||||
authProvider: authHandle?.provider,
|
||||
settings: body.settings,
|
||||
// Always intercept 401/403 on the node MCP transport. Without this, the
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ import type {
|
|||
JSONRPCMessage,
|
||||
MessageExtraInfo,
|
||||
} from "@modelcontextprotocol/client";
|
||||
import { SdkError, SdkErrorCode } from "@modelcontextprotocol/client";
|
||||
import { findHeader, isLongLivedStreamResponse } from "../fetchTracking.js";
|
||||
import type { InspectorServerSettings, StderrLogEntry } from "../types.js";
|
||||
import type { FetchRequestEntryBase } from "../types.js";
|
||||
import type { FetchRequestEntryBase, FetchStreamState } from "../types.js";
|
||||
import type {
|
||||
AuthChallenge,
|
||||
AuthChallengeOutcome,
|
||||
|
|
@ -61,6 +63,9 @@ export interface RemoteTransportOptions {
|
|||
/** Callback for async response-body updates to a previously tracked fetch. */
|
||||
onFetchResponseBody?: (id: string, responseBody: string) => void;
|
||||
|
||||
/** Callback for a previously tracked long-lived stream's events and close (#2318). */
|
||||
onFetchStreamUpdate?: (id: string, stream: FetchStreamState) => void;
|
||||
|
||||
/** Optional OAuth client provider for Bearer authentication */
|
||||
authProvider?: import("@modelcontextprotocol/client").OAuthClientProvider;
|
||||
|
||||
|
|
@ -261,6 +266,14 @@ export class RemoteClientTransport implements Transport {
|
|||
private eventStreamConsumeTask: Promise<void> | null = null;
|
||||
private restartingEventStream = false;
|
||||
private closed = false;
|
||||
/**
|
||||
* Long-lived streams the backend has reported open, by fetch entry id, with
|
||||
* the last event count seen for each. `close()` reports them closed
|
||||
* itself: it aborts the event channel before asking the backend to
|
||||
* disconnect, so the backend watcher's own close report lands on an
|
||||
* unconsumed queue and is lost with the session (#2318).
|
||||
*/
|
||||
private openStreams = new Map<string, number>();
|
||||
private readonly sseResponseWaits = new Map<
|
||||
string | number,
|
||||
SseResponseWait
|
||||
|
|
@ -539,12 +552,23 @@ export class RemoteClientTransport implements Transport {
|
|||
this.resetSseResponseWait(progressToken);
|
||||
}
|
||||
this.onmessage?.(msg, undefined);
|
||||
} else if (
|
||||
parsed.type === "fetch_request" &&
|
||||
this.options.onFetchRequest
|
||||
) {
|
||||
} else if (parsed.type === "fetch_request") {
|
||||
const entry = parsed.data;
|
||||
this.options.onFetchRequest({
|
||||
// Stream bookkeeping runs whether or not the consumer logs
|
||||
// requests: `onFetchStreamUpdate` is documented to work on its
|
||||
// own, and `close()` needs to know which streams are open. It
|
||||
// exists only to serve that callback, though — without one the
|
||||
// set would only ever grow, one entry per reconnect.
|
||||
if (
|
||||
this.options.onFetchStreamUpdate &&
|
||||
isLongLivedStreamResponse(
|
||||
entry.method,
|
||||
findHeader(entry.responseHeaders, "content-type"),
|
||||
)
|
||||
) {
|
||||
this.openStreams.set(entry.id, 0);
|
||||
}
|
||||
this.options.onFetchRequest?.({
|
||||
...entry,
|
||||
timestamp:
|
||||
typeof entry.timestamp === "string"
|
||||
|
|
@ -559,6 +583,19 @@ export class RemoteClientTransport implements Transport {
|
|||
parsed.data.id,
|
||||
parsed.data.responseBody,
|
||||
);
|
||||
} else if (
|
||||
parsed.type === "fetch_stream_update" &&
|
||||
this.options.onFetchStreamUpdate
|
||||
) {
|
||||
const { id, eventCount, closedAt } = parsed.data;
|
||||
if (closedAt !== undefined) this.openStreams.delete(id);
|
||||
else if (this.openStreams.has(id)) {
|
||||
this.openStreams.set(id, eventCount);
|
||||
}
|
||||
this.options.onFetchStreamUpdate(id, {
|
||||
eventCount,
|
||||
...(closedAt !== undefined && { closedAt: new Date(closedAt) }),
|
||||
});
|
||||
} else if (parsed.type === "stdio_log" && this.options.onStderr) {
|
||||
this.options.onStderr({
|
||||
timestamp: new Date(parsed.data.timestamp),
|
||||
|
|
@ -693,10 +730,17 @@ export class RemoteClientTransport implements Transport {
|
|||
const arm = () => {
|
||||
timer = setTimeout(() => {
|
||||
this.sseResponseWaits.delete(requestId);
|
||||
// The SDK's own per-request timeout shape, not a plain `Error`:
|
||||
// this wait can expire before the SDK's timer does (its budget is
|
||||
// the relay's, not the request's), and the rejection travels up
|
||||
// through `Client.request`, where `InspectorClient` annotates a
|
||||
// timeout of exactly this shape with the connection's state
|
||||
// (#2318). A plain error would pass that decorator untouched and
|
||||
// reach the user as a bare timeout on the web client alone.
|
||||
reject(
|
||||
new Error(
|
||||
`Timed out waiting for MCP response on SSE (${this.sseResponseTimeoutMs}ms)`,
|
||||
),
|
||||
new SdkError(SdkErrorCode.RequestTimeout, "Request timed out", {
|
||||
timeout: this.sseResponseTimeoutMs,
|
||||
}),
|
||||
);
|
||||
}, this.sseResponseTimeoutMs);
|
||||
};
|
||||
|
|
@ -836,6 +880,13 @@ export class RemoteClientTransport implements Transport {
|
|||
this.closed = true;
|
||||
this.cancelAllSseWaits(new Error("Transport closed"));
|
||||
await this.stopEventStream();
|
||||
// The disconnect below ends every stream the backend still holds open,
|
||||
// and its close reports cannot reach us any more (see `openStreams`).
|
||||
const closedAt = new Date();
|
||||
for (const [id, eventCount] of this.openStreams) {
|
||||
this.options.onFetchStreamUpdate?.(id, { eventCount, closedAt });
|
||||
}
|
||||
this.openStreams.clear();
|
||||
|
||||
if (this._sessionId) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -119,7 +119,10 @@ export class RemoteInspectorClientStorage implements InspectorClientStorage {
|
|||
|
||||
const data = (await res.json()) as InspectorClientSessionState;
|
||||
|
||||
// Deserialize state (convert ISO strings back to Date objects)
|
||||
// Deserialize state (convert ISO strings back to Date objects). The
|
||||
// nested `stream.closedAt` is a Date too, and `JSON.stringify` turned it
|
||||
// into an ISO string on the way out; left as a string it would fail the
|
||||
// `getTime()` every consumer of the type performs (#2318).
|
||||
return {
|
||||
...data,
|
||||
fetchRequests: data.fetchRequests.map((req) => ({
|
||||
|
|
@ -130,6 +133,9 @@ export class RemoteInspectorClientStorage implements InspectorClientStorage {
|
|||
: req.timestamp instanceof Date
|
||||
? req.timestamp
|
||||
: new Date(req.timestamp),
|
||||
...(req.stream?.closedAt !== undefined && {
|
||||
stream: { ...req.stream, closedAt: new Date(req.stream.closedAt) },
|
||||
}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ export type RemoteEventType =
|
|||
| "message"
|
||||
| "fetch_request"
|
||||
| "fetch_request_body_update"
|
||||
| "fetch_stream_update"
|
||||
| "stdio_log"
|
||||
| "transport_error"
|
||||
| "auth_challenge";
|
||||
|
|
@ -156,6 +157,16 @@ export interface RemoteEventFetchRequestBodyUpdate {
|
|||
data: { id: string; responseBody: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* A long-lived stream's lifecycle, forwarded from the backend's fetch tracker
|
||||
* (#2318). `closedAt` is an ISO string on the wire; the browser transport
|
||||
* rebuilds the `Date` the way it does for `fetch_request`'s timestamp.
|
||||
*/
|
||||
export interface RemoteEventFetchStreamUpdate {
|
||||
type: "fetch_stream_update";
|
||||
data: { id: string; eventCount: number; closedAt?: string };
|
||||
}
|
||||
|
||||
export interface RemoteEventStdioLog {
|
||||
type: "stdio_log";
|
||||
data: { timestamp: string; message: string };
|
||||
|
|
@ -178,6 +189,7 @@ export type RemoteEvent =
|
|||
| RemoteEventMessage
|
||||
| RemoteEventFetchRequest
|
||||
| RemoteEventFetchRequestBodyUpdate
|
||||
| RemoteEventFetchStreamUpdate
|
||||
| RemoteEventStdioLog
|
||||
| RemoteEventTransportError
|
||||
| RemoteEventAuthChallenge;
|
||||
|
|
|
|||
Loading…
Reference in a new issue