@@ -24,15 +24,27 @@ type ImmediateQueueEntry = { readonly kind: "immediate"; readonly entry: MockRes
2424
2525type DeferredQueueEntry = {
2626 readonly kind : "deferred" ;
27- /** Resolves when release() is called ( or pre-resolved if release ran first) . */
27+ /** Resolves when release() is called, or rejects when clear() runs . */
2828 readonly promise : Promise < IHumanResponse > ;
2929} ;
3030
3131type HarnessQueueEntry = ImmediateQueueEntry | DeferredQueueEntry ;
3232
33+ type DeferredHandle = {
34+ readonly settled : { done : boolean } ;
35+ readonly reject : ( err : Error ) => void ;
36+ } ;
37+
3338class HarnessScript implements MockResponseScript {
3439 private readonly _received : IHumanRequest [ ] = [ ] ;
3540 private readonly _queue : HarnessQueueEntry [ ] = [ ] ;
41+ /**
42+ * All deferreds that have been pushed but not yet settled. clear() rejects
43+ * each so any parked client.setRequestHandler `await next.promise` unblocks
44+ * deterministically — otherwise the SDK callback frame leaks until
45+ * client.close() at dispose.
46+ */
47+ private readonly _pendingDeferreds : DeferredHandle [ ] = [ ] ;
3648
3749 get received ( ) : ReadonlyArray < IHumanRequest > {
3850 return this . _received ;
@@ -48,15 +60,19 @@ class HarnessScript implements MockResponseScript {
4860
4961 pushDeferred ( ) : { release ( response : IHumanResponse ) : void } {
5062 let resolveFn : ( res : IHumanResponse ) => void = ( ) => { } ;
51- let settled = false ;
52- const promise = new Promise < IHumanResponse > ( ( resolve ) => {
63+ let rejectFn : ( err : Error ) => void = ( ) => { } ;
64+ const settled = { done : false } ;
65+ const promise = new Promise < IHumanResponse > ( ( resolve , reject ) => {
5366 resolveFn = resolve ;
67+ rejectFn = reject ;
5468 } ) ;
5569 this . _queue . push ( { kind : "deferred" , promise } ) ;
70+ const handle : DeferredHandle = { settled, reject : rejectFn } ;
71+ this . _pendingDeferreds . push ( handle ) ;
5672 return {
5773 release : ( response ) => {
58- if ( settled ) return ;
59- settled = true ;
74+ if ( settled . done ) return ;
75+ settled . done = true ;
6076 resolveFn ( response ) ;
6177 } ,
6278 } ;
@@ -66,9 +82,11 @@ class HarnessScript implements MockResponseScript {
6682 * Consumed by the elicit handler to resolve the next outgoing prompt.
6783 *
6884 * If a deferred entry is awaiting release, this awaits the deferred promise.
69- * The conformance suite's abort-mid-elicit test aborts the upstream signal
70- * BEFORE release is called — `Server.elicitInput` rejects on the connector
71- * side and the dangling promise is GC'd at dispose time. No leak.
85+ * abort-mid-elicit aborts the upstream signal first, but the parked
86+ * `await next.promise` here only unblocks when either `release()` runs or
87+ * `clear()` rejects the deferred. clear() rejects pending deferreds so the
88+ * handler frame unblocks deterministically between tests rather than
89+ * leaking until dispose.
7290 */
7391 async takeNext ( req : IHumanRequest ) : Promise < IHumanResponse > {
7492 const next = this . _queue . shift ( ) ;
@@ -84,6 +102,14 @@ class HarnessScript implements MockResponseScript {
84102 }
85103
86104 clear ( ) : void {
105+ for ( const handle of this . _pendingDeferreds ) {
106+ if ( handle . settled . done ) continue ;
107+ handle . settled . done = true ;
108+ const err = new Error ( "MCP harness cleared while deferred was pending" ) ;
109+ err . name = "AbortError" ;
110+ handle . reject ( err ) ;
111+ }
112+ this . _pendingDeferreds . length = 0 ;
87113 this . _received . length = 0 ;
88114 this . _queue . length = 0 ;
89115 }
0 commit comments