modelcontextprotocol / modelcontextprotocol/python-sdk

FastMCP server with SSE transport fails to shut down on a signal

Open
#514 12 comments 6 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug P3 ready for work
Dominant language
Python
Stars
24.3k
Forks
4k
Avg merge
1d 1h
Merged PRs (30d)
31

Description

A very simple server fails to shut down on a signal if it processed at least one request:

❯ python test-server.py --port=8085
INFO:     Started server process [216035]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8085 (Press CTRL+C to quit)
INFO:     127.0.0.1:55188 - "GET /sse HTTP/1.1" 200 OK
INFO:     127.0.0.1:55192 - "POST /messages/?session_id=e690de9733914d09aebf2b0e8c78191c HTTP/1.1" 202 Accepted
INFO:     127.0.0.1:55192 - "POST /messages/?session_id=e690de9733914d09aebf2b0e8c78191c HTTP/1.1" 202 Accepted
INFO:     127.0.0.1:55192 - "POST /messages/?session_id=e690de9733914d09aebf2b0e8c78191c HTTP/1.1" 202 Accepted
Processing request of type CallToolRequest
^CINFO:     Shutting down
INFO:     Waiting for background tasks to complete. (CTRL+C to force quit)

The expected behaviour would be:

❯ python test-server.py --port=8085
INFO:     Started server process [216006]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8085 (Press CTRL+C to quit)
^CINFO:     Shutting down
INFO:     Waiting for application shutdown.
INFO:     Application shutdown complete.
INFO:     Finished server process [216006]

Here is the code for both the server and the client.

What do I miss?

the server

import click
import sys
from pydantic import Field
from mcp.server.fastmcp import FastMCP

@click.command()
@click.option("--port", default=8085, help="Port to listen", type=int)
def main(port: int):
    mcp = FastMCP(
        "mcp-echo-tool",
        debug=True,
        log_level="INFO",
        port=port,
    )

    @mcp.tool(name="echo")
    async def echo_tool(string: str = Field(description="A string to echo back")) -> str:
        """Echoes back the input string"""
        return string

    mcp.run(transport='sse')
    return 0

if __name__ == "__main__":
    sys.exit(main())

and the client

import asyncio
import click
import sys
from mcp.client.session import ClientSession
from mcp.client.sse import sse_client

async def client(url, string):
    async with sse_client(url) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            result = await session.call_tool("echo", {"string": string})
            print(result.content)

@click.command()
@click.option('--url', default='http://localhost:8085', help='MCP Server URL', type=str)
@click.argument('string', type=str)
def main(url: str, string: str):
    asyncio.run(client(url, string))

if __name__ == "__main__":
    sys.exit(main())

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the shutdown behavior with the provided FastMCP SSE server and client, first confirming that a request has been processed before sending the signal. Then trace the FastMCP SSE server lifecycle and its Uvicorn shutdown path; done means the process reaches application shutdown and finishes without requiring a second signal.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.