-
Notifications
You must be signed in to change notification settings - Fork 1.8k
fix: async onclose, stdin EOF detection, SIGTERM in examples #1814
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| --- | ||
| "@modelcontextprotocol/core": patch | ||
| "@modelcontextprotocol/server": patch | ||
| "@modelcontextprotocol/client": patch | ||
| --- | ||
|
|
||
| Allow async `onclose` callbacks on Transport and Protocol. The signature changes from `() => void` to `() => void | Promise<void>`, and all call sites now await the callback. This lets MCP servers perform async cleanup (e.g., releasing browser sessions or database connections) when the transport closes. | ||
|
|
||
| Close `StdioServerTransport` when stdin reaches EOF, so containerized servers exit cleanly on client disconnect. | ||
|
|
||
| Add SIGTERM handlers alongside SIGINT in all examples, since MCP servers run as background processes stopped by SIGTERM, not interactively via Ctrl+C. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -316,7 +316,7 @@ | |
| * | ||
| * This is invoked when {@linkcode Protocol.close | close()} is called as well. | ||
| */ | ||
| onclose?: () => void; | ||
| onclose?: () => void | Promise<void>; | ||
|
|
||
| /** | ||
| * Callback for when an error occurs. | ||
|
|
@@ -456,11 +456,11 @@ | |
| async connect(transport: Transport): Promise<void> { | ||
| this._transport = transport; | ||
| const _onclose = this.transport?.onclose; | ||
| this._transport.onclose = () => { | ||
| this._transport.onclose = async () => { | ||
| try { | ||
| _onclose?.(); | ||
| if (_onclose) await _onclose(); | ||
| } finally { | ||
| this._onclose(); | ||
| await this._onclose(); | ||
| } | ||
| }; | ||
|
|
||
|
|
@@ -490,15 +490,18 @@ | |
| await this._transport.start(); | ||
| } | ||
|
|
||
| private _onclose(): void { | ||
| private async _onclose(): Promise<void> { | ||
| const responseHandlers = this._responseHandlers; | ||
| this._responseHandlers = new Map(); | ||
| this._progressHandlers.clear(); | ||
| this._taskManager.onClose(); | ||
| this._pendingDebouncedNotifications.clear(); | ||
| this._transport = undefined; | ||
|
|
||
| await this.onclose?.(); | ||
|
|
||
| for (const info of this._timeoutInfo.values()) { | ||
| clearTimeout(info.timeoutId); | ||
|
Check failure on line 504 in packages/core/src/shared/protocol.ts
|
||
|
Comment on lines
+493
to
504
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 The refactored Extended reasoning...The BugThe The Code PathIn
Why Existing Code Doesn't Prevent ItThe outer ImpactIf a user supplies an async
Step-by-Step Proof
FixWrap the try {
await this.onclose?.();
} finally {
for (const info of this._timeoutInfo.values()) { clearTimeout(info.timeoutId); }
this._timeoutInfo.clear();
const requestHandlerAbortControllers = this._requestHandlerAbortControllers;
this._requestHandlerAbortControllers = new Map();
const error = new SdkError(SdkErrorCode.ConnectionClosed, 'Connection closed');
for (const handler of responseHandlers.values()) { handler(error); }
for (const controller of requestHandlerAbortControllers.values()) { controller.abort(error); }
} |
||
| } | ||
| this._timeoutInfo.clear(); | ||
|
|
||
|
|
@@ -507,18 +510,12 @@ | |
|
|
||
| const error = new SdkError(SdkErrorCode.ConnectionClosed, 'Connection closed'); | ||
|
|
||
| this._transport = undefined; | ||
|
|
||
| try { | ||
| this.onclose?.(); | ||
| } finally { | ||
| for (const handler of responseHandlers.values()) { | ||
| handler(error); | ||
| } | ||
| for (const handler of responseHandlers.values()) { | ||
| handler(error); | ||
| } | ||
|
|
||
| for (const controller of requestHandlerAbortControllers.values()) { | ||
| controller.abort(error); | ||
| } | ||
| for (const controller of requestHandlerAbortControllers.values()) { | ||
| controller.abort(error); | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 The SIGTERM handler in
elicitationUrlExample.ts(line 817) andsimpleStreamableHttp.tslogs'\nReceived SIGINT. Cleaning up...'instead of'\nReceived SIGTERM. Cleaning up...'. This copy-paste error means operators monitoring logs will see misleading signal names when the process is stopped via SIGTERM (e.g., by container runtimes or process managers).Extended reasoning...
What the bug is: In both examples/client/src/elicitationUrlExample.ts and examples/client/src/simpleStreamableHttp.ts, the newly-added SIGTERM handler was copy-pasted from the SIGINT handler but the log string was not updated. It still reads 'Received SIGINT. Cleaning up...' inside process.on('SIGTERM', ...).