Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/client/stdio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,13 @@ export class StdioClientTransport implements Transport {
return this._process?.stderr ?? null;
}

/**
* The process id of the child process, if it has been started.
*/
get pid(): number | undefined {
return this._process?.pid;
}

private processReadBuffer() {
while (true) {
try {
Expand All @@ -187,6 +194,7 @@ export class StdioClientTransport implements Transport {

async close(): Promise<void> {
this._abortController.abort();
process.kill(this._process!.pid!, 'SIGKILL');
this._process = undefined;
this._readBuffer.clear();
}
Expand Down
39 changes: 37 additions & 2 deletions src/integration-tests/process-cleanup.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { execSync } from "node:child_process";
import { Server } from "../server/index.js";
import { StdioServerTransport } from "../server/stdio.js";
import { Client } from "../client/index.js";
import { StdioClientTransport } from "../client/stdio.js";

describe("Process cleanup", () => {
jest.setTimeout(5000); // 5 second timeout

it("should exit cleanly after closing transport", async () => {
it("server should exit cleanly after closing transport", async () => {
const server = new Server(
{
name: "test-server",
Expand All @@ -25,4 +28,36 @@ describe("Process cleanup", () => {
// The test runner will fail if the process hangs
expect(true).toBe(true);
});
});

it("client should exit cleanly after closing transport", async () => {
const isProcessRunning = (pid: number) => {
try {
execSync(`ps -p ${pid}`, { stdio: 'ignore' });
return true;

/* eslint-disable @typescript-eslint/no-unused-vars */
} catch (error) {
return false;
}
}

const client = new Client({
name: "test-client",
version: "1.0.0",
});

const transport = new StdioClientTransport({
command: "node",
args: ["server-that-hangs.js"],
cwd: __dirname
});

await client.connect(transport);
const pid = transport.pid;

await client.close();
await new Promise(resolve => setTimeout(resolve, 1000));

expect(isProcessRunning(pid!)).toBe(false);
});
});
21 changes: 21 additions & 0 deletions src/integration-tests/server-that-hangs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { setTimeout } from 'node:timers'
import process from 'node:process'
import { McpServer } from "../../dist/esm/server/mcp.js";
import { StdioServerTransport } from "../../dist/esm/server/stdio.js";

const transport = new StdioServerTransport();

const server = new McpServer({
name: "test-stdio-server",
version: "1.0.0"
});

await server.connect(transport);

const doNotExitImmediately = async () => {
setTimeout(() => process.exit(0), 30 * 1000);
};

process.stdin.on('close', doNotExitImmediately);
process.on('SIGINT', doNotExitImmediately);
process.on('SIGTERM', doNotExitImmediately);