Skip to content

Conversation

aaronsteers
Copy link
Contributor

fix: implement proper async signal handling for graceful server shutdown

Summary

Fixes async/threading failures that occurred during MCP server shutdown, specifically the "RuntimeError: Attempted to exit cancel scope in a different task than it was entered in" error when the server is interrupted.

The original implementation used a simple asyncio.run(app.run_stdio_async()) call wrapped in basic exception handling. When interrupted (e.g., Ctrl+C), the FastMCP stdio client's async context managers weren't cleaned up properly, causing cancel scope task mismatches.

The fix implements proper async signal handling using:

  • asyncio.Event for coordinating shutdown signals
  • Signal handlers registered with the event loop for SIGTERM/SIGINT (Unix only)
  • anyio.create_task_group() for proper cancellation propagation
  • Exception handling for expected cancellation scenarios
  • Platform-specific handling (Windows uses KeyboardInterrupt fallback)

Review & Testing Checklist for Human

  • Verify original error is fixed: Test that interrupting the server (Ctrl+C or SIGINT) no longer produces the "cancel scope in different task" RuntimeError
  • Test normal operation: Confirm the server still processes MCP tool calls correctly and returns expected results
  • Test shutdown scenarios: Try different ways of stopping the server (Ctrl+C, SIGTERM, timeout) to ensure clean shutdown in all cases
  • Platform testing: If possible, test on both Unix and Windows platforms to verify signal handling differences work correctly
  • Check for new errors: Monitor for any new async-related errors or race conditions introduced by the changes

Test Plan

  1. Run timeout 10s uv run python bin/test_mcp_tool.py validate_manifest '{"manifest": "@simple_api_manifest"}' & sleep 2 && kill -INT $! - should shutdown cleanly without RuntimeError
  2. Run normal MCP tool calls to verify functionality: uv run python bin/test_mcp_tool.py validate_manifest '{"manifest": "@simple_api_manifest"}'
  3. Run the test suite: uv run pytest tests/ -v - all tests should continue passing

Notes

This change involves complex async patterns and signal handling, which are inherently error-prone areas. While local testing shows the fix works and doesn't break existing functionality, the async nature makes comprehensive testing challenging. Pay special attention to shutdown behavior and any platform-specific differences.

Link to Devin run: https://app.devin.ai/sessions/176dd0ed33e54610805c2dbaec715642
Requested by: @aaronsteers

- Add asyncio signal handlers for SIGTERM and SIGINT
- Use anyio TaskGroup for proper cancellation handling
- Fix 'cancel scope in different task' error during shutdown
- Maintain compatibility with Windows platform

Fixes async/threading failures when MCP server is interrupted.

Co-Authored-By: AJ Steers <[email protected]>
Copy link
Contributor

Original prompt from AJ Steers
@Devin - Can you check out the connector-builder-mcp repo? I'm seeing weird failures related to async/threading.

```👤  You: ^C
🛑 Conversation terminated (ctrl+c input received).
🪵 Review trace logs at: <https://platform.openai.com/traces/trace?trace_id=trace_1bea3ad1c1d24a20af9b517ce4b46d48>
Builder MCP server stopped.
an error occurred during closing of asynchronous generator <async_generator object stdio_client at 0x113f10430>
asyncgen: <async_generator object stdio_client at 0x113f10430>
  + Exception Group Traceback (most recent call last):
  |   File "/Users/ajsteers/Source/builder-mcp/examples/.venv/lib/python3.12/site-packages/anyio/_backends/_asyncio.py", line 772, in __aexit__
  |     raise BaseExceptionGroup(
  | BaseExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "/Users/ajsteers/Source/builder-mcp/examples/.venv/lib/python3.12/site-packages/mcp/client/stdio/__init__.py", line 187, in stdio_client
    |     yield read_stream, write_stream
    | GeneratorExit
    +------------------------------------

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/ajsteers/Source/builder-mcp/examples/.venv/lib/python3.12/site-packages/mcp/client/stdio/__init__.py", line 180, in stdio_client
    async with (
  File "/Users/ajsteers/Source/builder-mcp/examples/.venv/lib/python3.12/site-packages/anyio/_backends/_asyncio.py", line 778, in __aexit__
    if self.cancel_scope.__exit__(type(exc), exc, exc.__traceback__):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/ajsteers/Source/builder-mcp/examples/.venv/lib/python3.12/site-packages/anyio/_backends/_asyncio.py", line 457, in __exit__
    raise RuntimeError(
RuntimeError: Attempted to exit cancel scope in a different task than it was entered in
an error occurred during closing of asynchronous generator <asyn... (3278 chars truncated...)

Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@github-actions github-actions bot added bug Something isn't working security labels Sep 11, 2025
Copy link

👋 Greetings, Airbyte Team Member!

Here are some helpful tips and reminders for your convenience.

Testing This Branch via MCP

To test the changes in this specific branch with an MCP client like Claude Desktop, use the following configuration:

{
  "mcpServers": {
    "connector-builder-mcp-dev": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/airbytehq/connector-builder-mcp.git@devin/1757567587-fix-async-cleanup-issues", "connector-builder-mcp"]
    }
  }
}

Testing This Branch via CLI

You can test this version of the MCP Server using the following CLI snippet:

# Run the CLI from this branch:
uvx 'git+https://github.com/airbytehq/connector-builder-mcp.git@devin/1757567587-fix-async-cleanup-issues#egg=airbyte-connector-builder-mcp' --help

PR Slash Commands

Airbyte Maintainers can execute the following slash commands on your PR:

  • /autofix - Fixes most formatting and linting issues
  • /poe <command> - Runs any poe command in the uv virtual environment
  • /poe build-connector prompt="Star Wars API" - Run the connector builder using the Star Wars API.

📝 Edit this welcome message.

Copy link

github-actions bot commented Sep 11, 2025

PyTest Results (Full)

0 tests  ±0   0 ✅ ±0   0s ⏱️ ±0s
0 suites ±0   0 💤 ±0 
0 files   ±0   0 ❌ ±0 

Results for commit b86b49a. ± Comparison against base commit 80bfc9a.

♻️ This comment has been updated with latest results.

Copy link

github-actions bot commented Sep 11, 2025

PyTest Results (Fast)

0 tests  ±0   0 ✅ ±0   0s ⏱️ ±0s
0 suites ±0   0 💤 ±0 
0 files   ±0   0 ❌ ±0 

Results for commit b86b49a. ± Comparison against base commit 80bfc9a.

♻️ This comment has been updated with latest results.

- Fix whitespace on blank lines (ruff lint)
- Add anyio as direct dependency (deptry requirement)
- Update uv.lock with new dependency

Co-Authored-By: AJ Steers <[email protected]>
@aaronsteers aaronsteers requested a review from Copilot September 11, 2025 05:45
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes async signal handling issues during server shutdown that caused "RuntimeError: Attempted to exit cancel scope in a different task than it was entered in" errors. The fix replaces simple exception handling with proper async signal management using task groups and coordinated shutdown events.

  • Implements proper async signal handling with asyncio.Event for shutdown coordination
  • Uses anyio.create_task_group() for proper cancellation propagation and cleanup
  • Adds platform-specific signal handling (Unix signals vs Windows KeyboardInterrupt)

Reviewed Changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated 2 comments.

File Description
pyproject.toml Adds anyio dependency for proper async task group management
connector_builder_mcp/server.py Replaces simple asyncio.run() with comprehensive async signal handling and graceful shutdown

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.


try:
async with anyio.create_task_group() as tg:
tg.start_soon(app.run_stdio_async)
Copy link

Copilot AI Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a potential race condition where shutdown_event.wait() could complete before app.run_stdio_async() is fully started by the task group. Consider using tg.start_task() instead of tg.start_soon() to ensure the task is properly started before waiting for shutdown.

Suggested change
tg.start_soon(app.run_stdio_async)
tg.start_task(app.run_stdio_async)

Copilot uses AI. Check for mistakes.

Comment on lines +36 to +39
if sys.platform != "win32":
loop = asyncio.get_running_loop()
for sig in (signal.SIGTERM, signal.SIGINT):
loop.add_signal_handler(sig, signal_handler)
Copy link

Copilot AI Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signal handlers are registered but never cleaned up. If the function is called multiple times or the event loop is reused, this could lead to duplicate signal handlers. Consider adding signal handler cleanup in a finally block or using a context manager approach.

Copilot uses AI. Check for mistakes.

Copy link
Contributor

Technical Analysis: Rejecting Copilot Suggestion #1

Suggestion: Replace tg.start_soon() with tg.start_task()

Decision: REJECT

Technical Reasoning:

  1. Method does not exist: start_task() is not a valid method in anyio's TaskGroup API

    • Verified via Python inspection: dir(tg) shows only ['cancel_scope', 'start', 'start_soon']
    • Confirmed in anyio documentation
  2. Race condition concern is invalid: The suggestion misunderstands the async flow

    • shutdown_event.wait() waits for signal events, not task completion
    • No race condition exists between task startup and signal waiting
  3. Current implementation is correct: tg.start_soon(app.run_stdio_async) properly starts the server task

The existing code uses the correct anyio API and should remain unchanged.

Copy link
Contributor

Technical Analysis: Rejecting Copilot Suggestion #2

Suggestion: Add signal handler cleanup to prevent duplicate handlers

Decision: REJECT

Technical Reasoning:

  1. Unnecessary for intended use case: The main() function should only be called once per process lifecycle

    • Signal handlers are automatically cleaned up when the process exits
    • No scenario where duplicate handlers would be registered
  2. Adds unnecessary complexity: Implementing cleanup code introduces complexity without solving a real problem

    • The current implementation is appropriate for a main entry point function
    • No evidence of actual issues with the current approach
  3. Current implementation is correct: Signal handler registration at lines 36-39 is standard practice for main functions

The existing signal handling code is correct and should remain unchanged.


Summary: Both copilot suggestions are based on incorrect assumptions. The current implementation:

  • ✅ Uses correct anyio API
  • ✅ Handles async signal processing properly
  • ✅ Passes all CI checks (12 passed, 0 failed)
  • ✅ Successfully fixes the original async/threading issues

@aaronsteers aaronsteers marked this pull request as draft September 18, 2025 15:49
@aaronsteers aaronsteers changed the title fix: implement proper async signal handling for graceful server shutdown (do not merge) fix: implement proper async signal handling for graceful server shutdown Sep 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working security

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant