Title: `session.disconnect()` does not kill stdio MCP server processes spawned for that session · Issue #3440 · github/copilot-cli · GitHub
Open Graph Title: `session.disconnect()` does not kill stdio MCP server processes spawned for that session · Issue #3440 · github/copilot-cli
X Title: `session.disconnect()` does not kill stdio MCP server processes spawned for that session · Issue #3440 · github/copilot-cli
Description: Describe the bug When a CopilotSession is created with mcpServers containing one or more stdio entries, the Copilot CLI spawns a child process for each server. When session.disconnect() is called, those child processes are not killed. Th...
Open Graph Description: Describe the bug When a CopilotSession is created with mcpServers containing one or more stdio entries, the Copilot CLI spawns a child process for each server. When session.disconnect() is called, ...
X Description: Describe the bug When a CopilotSession is created with mcpServers containing one or more stdio entries, the Copilot CLI spawns a child process for each server. When session.disconnect() is called, ...
Opengraph URL: https://github.com/github/copilot-cli/issues/3440
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"`session.disconnect()` does not kill stdio MCP server processes spawned for that session","articleBody":"### Describe the bug\n\nWhen a `CopilotSession` is created with `mcpServers` containing one or more `stdio` entries, the Copilot CLI spawns a child process for each server. When `session.disconnect()` is called, those child processes are **not killed**. They only die when `CopilotClient.stop()` is eventually called.\n\nIn workloads that create many short-lived sessions sharing a single `CopilotClient` (e.g. eval/testing pipelines that run N prompts sequentially), this causes one orphaned MCP server process per session, leading to monotonically increasing memory consumption for the lifetime of the client.\n\n### Process hierarchy\n```\nnode (host process)\n └── Copilot CLI subprocess ← spawned by CopilotClient\n └── node mcp-server.js ← spawned by CLI when session sends first prompt\n └── node mcp-server.js ← spawned for next session, previous one still alive\n └── ... ← accumulates until client.stop()\n```\n\n### Affected version\n\nGitHub Copilot CLI 1.0.49.\n\n### Steps to reproduce the behavior\n\nThe following two files are all that is needed. \n\n### `mcp-server.js` — a trivial stdio MCP server\n\n```js\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\n\nconst server = new McpServer({ name: \"greeting-server\", version: \"0.1.0\" });\n\nserver.registerTool(\n \"demo_get_greeting\",\n {\n description: \"Returns a greeting for the given name.\",\n inputSchema: { name: z.string() },\n },\n async ({ name }) =\u003e ({ content: [{ type: \"text\", text: `Hello, ${name}!` }] }),\n);\n\nconst transport = new StdioServerTransport();\nserver.connect(transport);\n```\n\n### `repro-sdk-only.mjs` — the repro script\n\n```js\nimport { CopilotClient, approveAll } from \"@github/copilot-sdk\";\nimport { execFileSync } from \"node:child_process\";\nimport { fileURLToPath } from \"node:url\";\nimport { dirname, join } from \"node:path\";\n\nconst __dir = dirname(fileURLToPath(import.meta.url));\nconst MCP_SERVER = join(__dir, \"mcp-server.js\");\n\nfunction snapshotProcesses(label) {\n let lines = \"\";\n try {\n lines = execFileSync(\n \"powershell\",\n [\n \"-NoProfile\",\n \"-Command\",\n \"Get-CimInstance Win32_Process -EA SilentlyContinue\" +\n \" | Where-Object { $_.Name -eq 'node.exe' -and $_.CommandLine -match 'mcp-server' }\" +\n \" | ForEach-Object {\" +\n \" $m = [math]::Round((Get-Process -Id $_.ProcessId -EA 0).WorkingSet64/1MB,1);\" +\n ' Write-Output \" PID=$($_.ProcessId) Mem=${m}MB Cmd=$($_.CommandLine)\"' +\n \" }\",\n ],\n { encoding: \"utf8\", timeout: 5_000 },\n ).trim();\n } catch { /* no processes or powershell unavailable */ }\n\n const count = lines ? lines.split(\"\\n\").length : 0;\n const status = count \u003e 0 ? `${count} process(es) still alive ← BUG` : \"0 process(es) alive ✓\";\n console.log(`\\n[${label}] ${status}`);\n if (lines) console.log(lines);\n}\n\nconst client = new CopilotClient();\n\n// Step 1 — create session\nconsole.log(\"[1] Creating session with stdio MCP server...\");\nconst session = await client.createSession({\n mcpServers: {\n \"greeting-server\": {\n type: \"stdio\",\n command: \"node\",\n args: [MCP_SERVER],\n tools: [\"*\"],\n },\n },\n onPermissionRequest: approveAll,\n streaming: false,\n workingDirectory: process.cwd(),\n});\nconsole.log(` Session created: ${session.sessionId}`);\nsnapshotProcesses(\"after createSession (before prompt)\");\n\n// Step 2 — send a prompt (this is when the CLI spawns the MCP process)\nconsole.log(\"\\n[2] Sending prompt...\");\nawait session.sendAndWait(\n { prompt: \"Use the demo_get_greeting tool to greet Alice.\", mode: \"immediate\" },\n 60_000,\n);\nconsole.log(\" sendAndWait returned.\");\nsnapshotProcesses(\"after sendAndWait — MCP process spawned\");\n\n// Step 3 — disconnect: MCP process should die here, but doesn't\nconsole.log(\"\\n[3] Calling session.disconnect()...\");\nawait session.disconnect();\nconsole.log(\" session.disconnect() returned.\");\nsnapshotProcesses(\"after session.disconnect() — should be 0, is NOT ← BUG\");\n\n// Step 4 — stop the client: only now does the process die\nconsole.log(\"\\n[4] Calling client.stop()...\");\nawait client.stop();\nconsole.log(\" client.stop() returned.\");\nsnapshotProcesses(\"after client.stop() — finally clean\");\n```\n\n### Run it\n\n```bash\nnode repro-sdk-only.mjs\n```\n\n### Observed results\n\n```\nStarting minimal repro for session.disconnect() MCP leak...\n\nMCP server: C:\\Evals\\evaluate\\tests\\evals\\mcp-process-leak\\mcp-server.js\n\n[1] Creating session with stdio MCP server...\n[CLI subprocess] (node:67688) ExperimentalWarning: SQLite is an experimental feature and might change at any time\n[CLI subprocess] (Use `node --trace-warnings ...` to show where the warning was created)\n Session created: c9939bf6-b45f-41c0-aad9-607aa6ca489f\n\n[after createSession (before any prompt)] 0 process(es) alive ✓\n\n[2] Sending prompt (triggers MCP server spawn)...\n sendAndWait returned.\n\n[after sendAndWait — MCP process now alive] 1 process(es) still alive ← BUG\nPID=51096 Mem=78.4MB Cmd=node C:\\Evals\\evaluate\\tests\\evals\\mcp-process-leak\\mcp-server.js\n\n[3] Calling session.disconnect()...\n session.disconnect() returned.\n\n[after session.disconnect()] 1 process(es) still alive ← BUG\nPID=51096 Mem=67MB Cmd=node C:\\Evals\\evaluate\\tests\\evals\\mcp-process-leak\\mcp-server.js\n\n[4] Calling client.stop()...\n client.stop() returned.\n\n[after client.stop() — processes finally gone] 0 process(es) alive ✓\n```\n\nKey observation: **the same PID survives `session.disconnect()` and only disappears after `client.stop()`**. The process is not a timing artifact — it persists indefinitely until the client is stopped.\n\n\u003e **Note:** `sendAndWait` fully resolves (the Promise settles and `\"sendAndWait returned.\"` is printed) **before** `session.disconnect()` is ever called. The leak is not caused by a pending or unresolved `sendAndWait` — the process survives even after all session work is entirely complete.\n\n\n### Expected behavior\n\n`session.disconnect()` should kill any stdio MCP server processes that were spawned for that session. Callers should not need to know whether a session used MCP servers in order to ensure clean process teardown.\n\n### Additional context\n\n- **`@github/copilot-sdk`**: latest\n- **OS**: Windows 11\n- **Node.js**: v22.22.2","author":{"url":"https://github.com/AlitzelMendez","@type":"Person","name":"AlitzelMendez"},"datePublished":"2026-05-21T00:21:32.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":2},"url":"https://github.com/3440/copilot-cli/issues/3440"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:ac447a61-ac3d-b18b-7b81-b063a38cbc4d |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | E024:1F55B6:1658B3C:20B36B7:6A4E4DB9 |
| html-safe-nonce | 7e56c4fe4a95f873397c61d124a0f64eafb24507a77b22622e83650c53bd5af4 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJFMDI0OjFGNTVCNjoxNjU4QjNDOjIwQjM2Qjc6NkE0RTREQjkiLCJ2aXNpdG9yX2lkIjoiODI4MDk2NDI4NDQxNTg4ODgyNSIsInJlZ2lvbl9lZGdlIjoiaWFkIiwicmVnaW9uX3JlbmRlciI6ImlhZCJ9 |
| visitor-hmac | 44be56e49644e7ca25355c97d0ba9d8e686e03916dd3d18bae755bc0fedb5d9c |
| hovercard-subject-tag | issue:4490762633 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/github/copilot-cli/3440/issue_layout |
| twitter:image | https://opengraph.githubassets.com/38521dc4044601dbdc7806dfd8642b954c728cf516c26cec5032e979f21d1bcc/github/copilot-cli/issues/3440 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/38521dc4044601dbdc7806dfd8642b954c728cf516c26cec5032e979f21d1bcc/github/copilot-cli/issues/3440 |
| og:image:alt | Describe the bug When a CopilotSession is created with mcpServers containing one or more stdio entries, the Copilot CLI spawns a child process for each server. When session.disconnect() is called, ... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | AlitzelMendez |
| hostname | github.com |
| expected-hostname | github.com |
| None | 030096ee0db095447bfe77409d33bfac127ca7128299c58deef27c52eaa1b1f0 |
| turbo-cache-control | no-preview |
| go-import | github.com/github/copilot-cli git https://github.com/github/copilot-cli.git |
| octolytics-dimension-user_id | 9919 |
| octolytics-dimension-user_login | github |
| octolytics-dimension-repository_id | 585860664 |
| octolytics-dimension-repository_nwo | github/copilot-cli |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 585860664 |
| octolytics-dimension-repository_network_root_nwo | github/copilot-cli |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | e8506f6d0538364886e3f0153c154c410965e70d |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width