Skip to content

The cleanup procedure after "yield" in lifespan is unreachable on Windows #1027

Open
@ScatterTemple

Description

@ScatterTemple

Initial Checks

Description

Dear developpers,

I am developing with MCP SDK Python on Windows. I noticed that the resource cleanup process of my local process defined inside the lifespan is not executed at all. To investigate the issue, I tested with the minimal code below and found that the code after the yield statement is never executed, regardless of whether an error occurs or not.
I haven’t been able to test this on platforms other than Windows or with the SSE transport.

"""agent_runner.py

A minimum code to use MCP server.
Please run this file.

"""
import os
import sys
import asyncio
import logging
from dotenv import load_dotenv
from openai import AsyncOpenAI
from agents import Agent, Runner, OpenAIChatCompletionsModel
from agents.mcp import MCPServerStdio, MCPServerStdioParams

load_dotenv()  # loading OPENAI_API_KEY

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("agent_runner")

# disable library loggers
logging.getLogger('mcp').setLevel(logging.ERROR)
logging.getLogger('httpx').setLevel(logging.ERROR)


async def main():

    # Launch MCP server
    params = MCPServerStdioParams(
        command=sys.executable,
        args=[os.path.join(os.path.dirname(__file__), "srv_stdio.py")],
        env={},
        encoding="utf-8"
    )
    async with MCPServerStdio(params=params, name="MCP tool") as mcp_server:

        logger.info("4. MCP stdio server running")

        # Create agent
        agent = Agent(
            name="AgentWithMCP",
            instructions="Answer with using tools.",
            model=OpenAIChatCompletionsModel(model="gpt-4", openai_client=AsyncOpenAI()),
            mcp_servers=[mcp_server]
        )

        prompt = "Please reverse the word 'hello'."
        logger.info(f"  PROMPT: {prompt}")
        result = await Runner.run(agent, input=prompt)
        logger.info(f"  FINAL OUTPUT: {result.final_output}")


if __name__ == "__main__":
    logger.info("1. Starting program...")
    asyncio.run(main())
    logger.info("8. Program terminated.")
"""srv_stdio.py

A minimum code to show ignoring after "yield" in lifetime.
Please put this file on the same folder as "agent_runner.py".

"""
from __future__ import annotations as _annotations

import os
import sys
import logging
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from mcp.server.fastmcp import FastMCP
from mcp.server.lowlevel.server import Server, LifespanResultT, RequestT

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("stdio server")

# disable library loggers
logging.getLogger('mcp').setLevel(logging.ERROR)
logging.getLogger('httpx').setLevel(logging.ERROR)

if sys.platform == "win32" and os.environ.get('PYTHONIOENCODING') is None:
    sys.stdin.reconfigure(encoding="utf-8")
    sys.stdout.reconfigure(encoding="utf-8")
    sys.stderr.reconfigure(encoding="utf-8")


@asynccontextmanager
async def lifespan(server: Server[LifespanResultT, RequestT]) -> AsyncIterator[object]:
    logger.info('3. Launching Server...')
    try:
        yield {}
    finally:
        logger.info('6. Terminating Server '
                    '(Want to release my resources here).')  # not shown


mcp = FastMCP(
    "EchoMCPServer",
    lifespan=lifespan,
)


@mcp.tool()
def echo(msg: str) -> str:
    """Make the passed string reversed."""
    logger.info(f"5. [echo] called with msg='{msg}'")
    return msg[::-1]


if __name__ == "__main__":
    logger.info("2. Starting MCP server...")
    mcp.run()
    logger.info("7. MCP server terminated.")  # not shown

The outputs missing "6. Terminating Server ~" and "7. MCP server terminated.".

INFO:agent_runner:1. Starting program...
INFO:stdio server:2. Starting MCP server...
INFO:stdio server:3. Launching Server...
INFO:agent_runner:4. MCP stdio server running
INFO:agent_runner:  PROMPT: Please reverse the word 'hello'.
INFO:stdio server:5. [echo] called with msg='olleh'
INFO:agent_runner:  FINAL OUTPUT: The reverse of 'hello' is 'olleh'.
INFO:agent_runner:8. Program terminated.

I suspected that this issue might be due to the process being forcibly terminated when exiting the with block in MCPServerStdio. Upon investigation, I found that modifying the following section allows the sample code to correctly display "6. Terminating Server ~" and "7. MCP server terminated.", and the cleanup process is executed as intended.

# mcp/client/stdio/win32.py
102  -         process.terminate()
102  +         os.kill(process.pid, signal.CTRL_C_EVENT)

This change seems to resolve the issue on my end, but do you think it could be helpful for improving the MCP SDK overall?

Example Code

Python & MCP Python SDK

Python 3.10
mcp 1.9.4
openai-agents 0.0.17

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions