-
Notifications
You must be signed in to change notification settings - Fork 1.5k
fix hanging stdio servers #1200
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
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
896e9f3
fix: ensure stdio closes server
johnnyasantoss dbaf1e3
chore: add test to client close
johnnyasantoss 50434b7
chore: avoid multiple calls to onclose when terminating
johnnyasantoss e23c1f8
chore: close stdin before sending sigkill
johnnyasantoss 0b38067
chore: add mock stream cleanup
johnnyasantoss b93d564
chore: lint
johnnyasantoss a8f1a33
fix: use proper initialization for servers in tests
johnnyasantoss f95739f
chore: exclude integration tests from final build
johnnyasantoss ba97219
Merge branch 'main' into main
mattzcarey 539fb3d
Merge branch 'main' into main
mattzcarey 07ae296
fix: test stopping vitest exiting
mattzcarey 1eb9340
remove debug logs
mattzcarey 03dd7fa
fixtures folder
mattzcarey 62aa115
Merge branch 'main' into fix-hanging-stdio
mattzcarey 674e79f
match mcp spec and python
mattzcarey 40ddaa5
align with python and remove unused abost controller.
mattzcarey File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { setInterval } from 'node:timers'; | ||
| import process from 'node:process'; | ||
| import { McpServer } from '../server/mcp.js'; | ||
| import { StdioServerTransport } from '../server/stdio.js'; | ||
|
|
||
| const transport = new StdioServerTransport(); | ||
|
|
||
| const server = new McpServer( | ||
| { | ||
| name: 'server-that-hangs', | ||
| title: 'Test Server that hangs', | ||
| version: '1.0.0' | ||
| }, | ||
| { | ||
| capabilities: { | ||
| logging: {} | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| await server.connect(transport); | ||
|
|
||
| // Keep process alive even after stdin closes | ||
| const keepAlive = setInterval(() => {}, 60_000); | ||
|
|
||
| // Prevent transport close from exiting | ||
| transport.onclose = () => { | ||
| // Intentionally ignore - we want to test the signal handling | ||
| }; | ||
|
|
||
| const doNotExitImmediately = async (signal: NodeJS.Signals) => { | ||
| await server.sendLoggingMessage({ | ||
| level: 'debug', | ||
| data: `received signal ${signal}` | ||
| }); | ||
| // Clear keepalive but delay exit to simulate slow shutdown | ||
| clearInterval(keepAlive); | ||
| setInterval(() => {}, 30_000); | ||
| }; | ||
|
|
||
| process.on('SIGINT', doNotExitImmediately); | ||
| process.on('SIGTERM', doNotExitImmediately); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import { McpServer } from '../server/mcp.js'; | ||
| import { StdioServerTransport } from '../server/stdio.js'; | ||
|
|
||
| const transport = new StdioServerTransport(); | ||
|
|
||
| const server = new McpServer({ | ||
| name: 'test-server', | ||
| version: '1.0.0' | ||
| }); | ||
|
|
||
| await server.connect(transport); | ||
|
|
||
| const exit = async () => { | ||
| await server.close(); | ||
| process.exit(0); | ||
| }; | ||
|
|
||
| process.on('SIGINT', exit); | ||
| process.on('SIGTERM', exit); |
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| import path from 'node:path'; | ||
| import { Readable, Writable } from 'node:stream'; | ||
| import { Client } from '../client/index.js'; | ||
| import { StdioClientTransport } from '../client/stdio.js'; | ||
| import { Server } from '../server/index.js'; | ||
| import { StdioServerTransport } from '../server/stdio.js'; | ||
| import { LoggingMessageNotificationSchema } from '../types.js'; | ||
|
|
||
| const FIXTURES_DIR = path.resolve(__dirname, '../__fixtures__'); | ||
|
|
||
| describe('Process cleanup', () => { | ||
| vi.setConfig({ testTimeout: 5000 }); // 5 second timeout | ||
|
|
||
| it('server should exit cleanly after closing transport', async () => { | ||
| const server = new Server( | ||
| { | ||
| name: 'test-server', | ||
| version: '1.0.0' | ||
| }, | ||
| { | ||
| capabilities: {} | ||
| } | ||
| ); | ||
|
|
||
| const mockReadable = new Readable({ | ||
| read() { | ||
| this.push(null); // signal EOF | ||
| } | ||
| }), | ||
| mockWritable = new Writable({ | ||
| write(chunk, encoding, callback) { | ||
| callback(); | ||
| } | ||
| }); | ||
|
|
||
| // Attach mock streams to process for the server transport | ||
| const transport = new StdioServerTransport(mockReadable, mockWritable); | ||
| await server.connect(transport); | ||
|
|
||
| // Close the transport | ||
| await transport.close(); | ||
|
|
||
| // ensure a proper disposal mock streams | ||
| mockReadable.destroy(); | ||
| mockWritable.destroy(); | ||
|
|
||
| // If we reach here without hanging, the test passes | ||
| // The test runner will fail if the process hangs | ||
| expect(true).toBe(true); | ||
| }); | ||
|
|
||
| it('onclose should be called exactly once', async () => { | ||
| const client = new Client({ | ||
| name: 'test-client', | ||
| version: '1.0.0' | ||
| }); | ||
|
|
||
| const transport = new StdioClientTransport({ | ||
| command: 'node', | ||
| args: ['--import', 'tsx', 'testServer.ts'], | ||
| cwd: FIXTURES_DIR | ||
| }); | ||
|
|
||
| await client.connect(transport); | ||
|
|
||
| let onCloseWasCalled = 0; | ||
| client.onclose = () => { | ||
| onCloseWasCalled++; | ||
| }; | ||
|
|
||
| await client.close(); | ||
|
|
||
| // A short delay to allow the close event to propagate | ||
| await new Promise(resolve => setTimeout(resolve, 50)); | ||
|
|
||
| expect(onCloseWasCalled).toBe(1); | ||
| }); | ||
|
|
||
| it('should exit cleanly for a server that hangs', async () => { | ||
| const client = new Client({ | ||
| name: 'test-client', | ||
| version: '1.0.0' | ||
| }); | ||
|
|
||
| const transport = new StdioClientTransport({ | ||
| command: 'node', | ||
| args: ['--import', 'tsx', 'serverThatHangs.ts'], | ||
| cwd: FIXTURES_DIR | ||
| }); | ||
|
|
||
| await client.connect(transport); | ||
| await client.setLoggingLevel('debug'); | ||
| client.setNotificationHandler(LoggingMessageNotificationSchema, notification => { | ||
| console.debug('server log: ' + notification.params.data); | ||
| }); | ||
| const serverPid = transport.pid!; | ||
|
|
||
| await client.close(); | ||
|
|
||
| // A short delay to allow the close event to propagate | ||
| await new Promise(resolve => setTimeout(resolve, 50)); | ||
|
|
||
| try { | ||
| process.kill(serverPid, 9); | ||
| throw new Error('Expected server to be dead but it is alive'); | ||
| } catch (err: unknown) { | ||
| // 'ESRCH' the process doesn't exist | ||
| if (err && typeof err === 'object' && 'code' in err && err.code === 'ESRCH') { | ||
| // success | ||
| } else throw err; | ||
| } | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
🧑🍳 👌