René's URL Explorer Experiment


Title: MCP Server: Inconsistent Exception Handling in @app.call_tool and Client Undetected Server Termination via Stdio · Issue #396 · modelcontextprotocol/python-sdk · GitHub

Open Graph Title: MCP Server: Inconsistent Exception Handling in @app.call_tool and Client Undetected Server Termination via Stdio · Issue #396 · modelcontextprotocol/python-sdk

X Title: MCP Server: Inconsistent Exception Handling in @app.call_tool and Client Undetected Server Termination via Stdio · Issue #396 · modelcontextprotocol/python-sdk

Description: Describe the bug There are two distinct issues observed when using the MCP Python SDK with the stdio transport: Inconsistent Exception Handling: Exceptions raised within handlers decorated with @app.call_tool are not correctly translated...

Open Graph Description: Describe the bug There are two distinct issues observed when using the MCP Python SDK with the stdio transport: Inconsistent Exception Handling: Exceptions raised within handlers decorated with @ap...

X Description: Describe the bug There are two distinct issues observed when using the MCP Python SDK with the stdio transport: Inconsistent Exception Handling: Exceptions raised within handlers decorated with @ap...

Opengraph URL: https://github.com/modelcontextprotocol/python-sdk/issues/396

X: @github

direct link

Domain: github.com


Hey, it has json ld scripts:
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"MCP Server: Inconsistent Exception Handling in @app.call_tool and Client Undetected Server Termination via Stdio","articleBody":"**Describe the bug**\nThere are two distinct issues observed when using the MCP Python SDK with the stdio transport:\n\n1. Inconsistent Exception Handling: Exceptions raised within handlers decorated with @app.call_tool are not correctly translated into JSON-RPC error responses sent to the client. Instead, the server catches the exception and sends a successful response where the content field contains the exception's message as plain text. This contrasts with handlers like @app.list_resources, where exceptions are correctly translated into McpError responses received by the client.\n2. Undetected Server Termination: When the server process terminates abruptly (e.g., via sys.exit(1) during a tool call), the client connected via mcp.client.stdio.stdio_client does not detect the broken pipe or EOF. Client calls awaiting a response from the terminated server hang indefinitely until an application-level timeout (like asyncio.wait_for) expires, instead of raising a transport-level error (e.g., BrokenPipeError, EOFError, anyio.EndOfStream, etc.).\n\n\n**To Reproduce**\n\n1. Save the attached minimal_server.py and minimal_client.py files.\n2. Ensure the mcp library is installed (pip install mcp).\n3. Run the client from the command line: python -m minimal_client\n4. Observe the output:\n* The \"Testing list_resources\" step correctly shows an McpError being caught.\n* The \"Testing working_tool\" step correctly shows a successful call.\n* The \"Testing normal_error_tool\" step incorrectly shows a successful call, with the ValueError message appearing inside the Result: [TextContent(...)].\n* The \"Testing exit_tool\" step hangs for the duration of the asyncio.wait_for timeout (2 seconds in the example) and then prints the timeout error message, instead of failing immediately due to the server process termination.\n\n**Expected behavior**\n\n1. Consistent Exception Handling: Exceptions raised within @app.call_tool handlers should be treated the same way as exceptions in @app.list_resources. The server should send a standard JSON-RPC error response, which the client should receive as an McpError (or a subclass thereof). The client should not receive a successful response containing the error message text.\n2. Server Termination Detection: When the server process connected via stdio terminates unexpectedly, the client's read/write operations on the transport should fail immediately with an appropriate transport-level exception (like BrokenPipeError, EOFError, anyio.EndOfStream, anyio.BrokenResourceError, etc.), allowing the client application to detect and handle the disconnection promptly without relying on application-level timeouts for pending requests.\n\n**Screenshots**\nThe console output provided in the previous conversation turn serves as evidence for the actual behavior:\n```\nPS G:\\projects\\mcp-exp\u003e python -m minimal_client\nConnecting to server\n\n--- Testing list_resources ---\nCalling list_resources tool\nError calling list_resources\nMcpError: This is a deliberate error from list_resources\nTraceback (most recent call last):\n  File \"G:\\projects\\mcp-exp\\minimal_client.py\", line 74, in call_list_resources\n    response = await self.session.list_resources()\n               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"C:\\ProgramData\\Anaconda3\\envs\\py3128\\Lib\\site-packages\\mcp\\client\\session.py\", line 196, in list_resources\n    return await self.send_request(\n           ^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"C:\\ProgramData\\Anaconda3\\envs\\py3128\\Lib\\site-packages\\mcp\\shared\\session.py\", line 266, in send_request\n    raise McpError(response_or_error.error)\nmcp.shared.exceptions.McpError: This is a deliberate error from list_resources\n\n--- Testing working_tool ---\nCalling tool: working_tool\n\nSuccessfully called tool 'working_tool'\nResult: [TextContent(type='text', text='Hello from working_tool! Args: {}', annotations=None)]\n\n--- Testing normal_error_tool ---\nCalling tool: normal_error_tool\n\nSuccessfully called tool 'normal_error_tool'\nResult: [TextContent(type='text', text='This is a deliberate error from normal_error_tool', annotations=None)]\n\n--- Testing exit_tool ---\nCalling tool: exit_tool\nERROR calling tool 'exit_tool': Call timed out after 2.0s.\n   This is expected for 'exit_tool' due to undetected server termination.\nCleaning up client resources\n```\n\n**Desktop (please complete the following information):**\n - OS: Windows 11 (but likely affects other OS)\n - Environment: Python 3.12.8 (via Anaconda)\n - Version: MCP version 1.6.1.dev4+2ea1495\n\n\n**Additional context**\nThe minimal reproducible example files (minimal_client.py and minimal_server.py) are below (attaching .py isn't allowed). The key issue seems to be how the mcp.server handles exceptions differently based on the decorator used (@app.call_tool vs @app.list_resources) and how the mcp.client.stdio.stdio_client transport interacts with terminated subprocesses.\n\n`====minimal_server.py====`\n```python\n    import asyncio\n    import sys\n    import mcp.types as types\n    from mcp.server import Server\n    from mcp.server.stdio import stdio_server\n\n    # Create a minimal server\n    app = Server(\"minimal-server\")\n\n\n    @app.list_resources()\n    async def handle_list_resources() -\u003e list[types.Resource]:\n        \"\"\"Handles the list_resources request.\"\"\"\n        # Simulate a deliberate error for testing purposes\n        # This will be caught by MCP and sent back to client as an error\n        raise ValueError(\"This is a deliberate error from list_resources\")\n\n\n    @app.call_tool()\n    async def handle_tool_call(name: str, arguments: dict) -\u003e list:\n        \"\"\"Handles calls for working_tool, normal_error_tool, and exit_tool.\"\"\"\n\n        if name == \"working_tool\":\n            # Return the content part directly\n            return [\n                types.TextContent(type=\"text\", text=f'Hello from working_tool! Args: {arguments}')\n            ]\n        elif name == \"normal_error_tool\":\n            # Raise a standard Python exception\n            raise ValueError(\"This is a deliberate error from normal_error_tool\")\n            # No return needed here\n        elif name == \"exit_tool\":\n            # Abruptly terminate the server process.\n            sys.exit(1)\n            # No return needed here\n        else:\n            # Handle unknown tool names if necessary\n            raise ValueError(f\"Unknown tool: {name}\") # Or return an error content dict\n\n\n    # Main execution function\n    async def main():\n        async with stdio_server() as streams:\n            read_stream, write_stream = streams\n            await app.run(\n                read_stream,\n                write_stream,\n                app.create_initialization_options()\n            )\n\n\n    if __name__ == \"__main__\":\n        asyncio.run(main())\n```\n\n`====minimal_client.py====`\n```python\nimport asyncio\nimport traceback\nfrom typing import Optional\n\nfrom mcp import ClientSession, StdioServerParameters, McpError\nfrom mcp.client.stdio import stdio_client\nfrom contextlib import AsyncExitStack\n\n\nclass MinimalClient:\n    def __init__(self):\n        self.session: Optional[ClientSession] = None\n        self.exit_stack = AsyncExitStack()\n        self.stdio = None\n        self.write = None\n        \n    async def connect(self):\n        print(\"Connecting to server\")\n        \n        server_params = StdioServerParameters(\n            command=\"python\",\n            args=['minimal_server.py'],\n        )\n        \n        # Create stdio connection\n        stdio_transport = await self.exit_stack.enter_async_context(\n            stdio_client(server_params)\n        )\n        \n        self.stdio, self.write = stdio_transport\n        \n        # Create client session with timeout and log handler\n        self.session = await self.exit_stack.enter_async_context(\n            ClientSession(\n                self.stdio,\n                self.write\n           )\n        )\n    \n        await self.session.initialize()\n            \n        \n    async def call_tool(self, tool_name: str, timeout_seconds: float = 2.0):\n        \"\"\"Call a tool with timeout\"\"\"\n        if not self.session:\n            raise RuntimeError(\"Client not connected to server\")\n            \n        print(f\"Calling tool: {tool_name}\")\n        try:\n            #response = await self.session.call_tool(name=tool_name, arguments={})             \n            response = await asyncio.wait_for(\n                self.session.call_tool(name=tool_name, arguments={}),\n                timeout=timeout_seconds\n            )            \n            print(f\"\\nSuccessfully called tool '{tool_name}'\")\n            print(f\"Result: {response.content}\")\n            return response.content\n        except TimeoutError: # *** Catch TimeoutError ***\n            print(f\"ERROR calling tool '{tool_name}': Call timed out after {timeout_seconds}s.\")\n            print(\"   This is expected for 'exit_tool' due to undetected server termination.\")\n        except McpError as e:\n            print(f\"\\nERROR calling tool '{tool_name}':\")\n            print(f\"{type(e).__name__}: {e}\")\n            traceback.print_exc()\n\n\n    async def call_list_resources(self):\n        \"\"\"Call list_resources tool\"\"\"\n        if not self.session:\n            raise RuntimeError(\"Client not connected to server\")\n            \n        print(\"Calling list_resources tool\")\n        try:\n            response = await self.session.list_resources()\n            print(f\"Resources: {response.resources}\")\n            return response.resources\n        except McpError as e:\n            print(f\"Error calling list_resources\")\n            print(f\"{type(e).__name__}: {e}\")\n            traceback.print_exc()\n            \n            \n    async def cleanup(self):\n        \"\"\"Clean up resources\"\"\"\n        print(\"Cleaning up client resources\")\n        await self.exit_stack.aclose()\n        \n\nasync def main():\n    \n   \n    client = MinimalClient()\n    try:\n        # Connect to server\n        await client.connect()\n\n        # Test list_resources that raises an error (This is a deliberate error from handle_list_resources)\n        print(\"\\n--- Testing list_resources ---\")\n        # Ideal: Expecting the client to receive an error representing the server's ValueError\n        # Actual = Ideal (The client correctly receives an McpError, matching the ideal behavior for this specific handler type)\n        await client.call_list_resources()\n\n        # Test working tool first\n        print(\"\\n--- Testing working_tool ---\")\n        # Ideal: Expecting a successful call\n        # Actual = Ideal (The call succeeds as expected)\n        await client.call_tool(\"working_tool\")\n\n        # Test normal error tool (This is a deliberate error from normal_error_tool)\n        print(\"\\n--- Testing normal_error_tool ---\")\n        # Ideal: Expecting the client to receive an error representing the server's ValueError\n        # Actual: The client incorrectly receives a successful response containing the error message text, highlighting the inconsistent error handling for @app.call_tool.\n        await client.call_tool(\"normal_error_tool\")\n\n        # Test exit tool\n        print(\"\\n--- Testing exit_tool ---\")\n        # Ideal: Expecting the client to detect the broken stdio pipe, likely resulting in a transport-level error (e.g., BrokenPipeError, EOFError, or similar).\n        # Actual: The client fails to detect the termination and hangs until the asyncio.wait_for timeout occurs, demonstrating the core issue with termination detection.\n        await client.call_tool(\"exit_tool\")\n\n\n    except Exception as e:\n        print(f\"\\nError in main execution: {type(e).__name__}: {e}\")\n        # traceback.print_exc() # Uncomment for full traceback if needed\n    finally:\n        await client.cleanup()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```","author":{"url":"https://github.com/omasoud","@type":"Person","name":"omasoud"},"datePublished":"2025-03-31T04:58:36.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":1},"url":"https://github.com/396/python-sdk/issues/396"}

route-pattern/_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format)
route-controllervoltron_issues_fragments
route-actionissue_layout
fetch-noncev2:8642aace-1b2b-1b03-7a57-da9a91a27175
current-catalog-service-hash81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114
request-idCFF6:EC4FB:34307:47004:6A5A6463
html-safe-nonceaf6022c44041290766bab972feb2bf7062201b628a03dca695b0c23ca745a283
visitor-payloadeyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJDRkY2OkVDNEZCOjM0MzA3OjQ3MDA0OjZBNUE2NDYzIiwidmlzaXRvcl9pZCI6Ijc5MzIxODU0OTUxNDA0NTk2MTkiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ==
visitor-hmac1bc08a000f7e7a149de2a05d1962b882aec81bc1e360c36b9827e7a5723d1fff
hovercard-subject-tagissue:2959475133
github-keyboard-shortcutsrepository,issues,copilot
google-site-verificationApib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I
octolytics-urlhttps://collector.github.com/github/collect
analytics-location///voltron/issues_fragments/issue_layout
fb:app_id1401488693436528
apple-itunes-appapp-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/modelcontextprotocol/python-sdk/396/issue_layout
twitter:imagehttps://opengraph.githubassets.com/4de2962c2b9b577462eae45b410030d1167c7cabacc018188a56375f2e7d6f73/modelcontextprotocol/python-sdk/issues/396
twitter:cardsummary_large_image
og:imagehttps://opengraph.githubassets.com/4de2962c2b9b577462eae45b410030d1167c7cabacc018188a56375f2e7d6f73/modelcontextprotocol/python-sdk/issues/396
og:image:altDescribe the bug There are two distinct issues observed when using the MCP Python SDK with the stdio transport: Inconsistent Exception Handling: Exceptions raised within handlers decorated with @ap...
og:image:width1200
og:image:height600
og:site_nameGitHub
og:typeobject
og:author:usernameomasoud
hostnamegithub.com
expected-hostnamegithub.com
None05b9ddf6a47d2dbe13944873a99f5fb4b83ba4871f9cb8a8e256793a63ca9687
turbo-cache-controlno-preview
go-importgithub.com/modelcontextprotocol/python-sdk git https://github.com/modelcontextprotocol/python-sdk.git
octolytics-dimension-user_id182288589
octolytics-dimension-user_loginmodelcontextprotocol
octolytics-dimension-repository_id862584018
octolytics-dimension-repository_nwomodelcontextprotocol/python-sdk
octolytics-dimension-repository_publictrue
octolytics-dimension-repository_is_forkfalse
octolytics-dimension-repository_network_root_id862584018
octolytics-dimension-repository_network_root_nwomodelcontextprotocol/python-sdk
turbo-body-classeslogged-out env-production page-responsive
disable-turbofalse
browser-stats-urlhttps://api.github.com/_private/browser/stats
browser-errors-urlhttps://api.github.com/_private/browser/errors
releasef8d29d1bd03dda2dd14b3f80b8bc27e1111f43bd
ui-targetfull
theme-color#1e2327
color-schemelight dark

Links:

Skip to contenthttps://github.com/modelcontextprotocol/python-sdk/issues/396#start-of-content
https://github.com/
Sign in https://github.com/login?return_to=https%3A%2F%2Fgithub.com%2Fmodelcontextprotocol%2Fpython-sdk%2Fissues%2F396
GitHub CopilotWrite better code with AIhttps://github.com/features/copilot
GitHub Copilot appDirect agents from issue to mergehttps://github.com/features/ai/github-app
MCP RegistryNewIntegrate external toolshttps://github.com/mcp
ActionsAutomate any workflowhttps://github.com/features/actions
CodespacesInstant dev environmentshttps://github.com/features/codespaces
IssuesPlan and track workhttps://github.com/features/issues
Code ReviewManage code changeshttps://github.com/features/code-review
GitHub Advanced SecurityFind and fix vulnerabilitieshttps://github.com/security/advanced-security
Code securitySecure your code as you buildhttps://github.com/security/advanced-security/code-security
Secret protectionStop leaks before they starthttps://github.com/security/advanced-security/secret-protection
Why GitHubhttps://github.com/why-github
Documentationhttps://docs.github.com
Bloghttps://github.blog
Changeloghttps://github.blog/changelog
Marketplacehttps://github.com/marketplace
View all featureshttps://github.com/features
Enterpriseshttps://github.com/enterprise
Small and medium teamshttps://github.com/team
Startupshttps://github.com/enterprise/startups
Nonprofitshttps://github.com/solutions/industry/nonprofits
App Modernizationhttps://github.com/solutions/use-case/app-modernization
DevSecOpshttps://github.com/solutions/use-case/devsecops
DevOpshttps://github.com/solutions/use-case/devops
CI/CDhttps://github.com/solutions/use-case/ci-cd
View all use caseshttps://github.com/solutions/use-case
Healthcarehttps://github.com/solutions/industry/healthcare
Financial serviceshttps://github.com/solutions/industry/financial-services
Manufacturinghttps://github.com/solutions/industry/manufacturing
Governmenthttps://github.com/solutions/industry/government
View all industrieshttps://github.com/solutions/industry
View all solutionshttps://github.com/solutions
AIhttps://github.com/resources/articles?topic=ai
Software Developmenthttps://github.com/resources/articles?topic=software-development
DevOpshttps://github.com/resources/articles?topic=devops
Securityhttps://github.com/resources/articles?topic=security
View all topicshttps://github.com/resources/articles
Customer storieshttps://github.com/customer-stories
Events & webinarshttps://github.com/resources/events
Ebooks & reportshttps://github.com/resources/whitepapers
Business insightshttps://github.com/solutions/executive-insights
GitHub Skillshttps://skills.github.com
Documentationhttps://docs.github.com
Customer supporthttps://support.github.com
Community forumhttps://github.com/orgs/community/discussions
Trust centerhttps://github.com/trust-center
Partnershttps://github.com/partners
View all resourceshttps://github.com/resources
GitHub SponsorsFund open source developershttps://github.com/open-source/sponsors
Security Labhttps://securitylab.github.com
Maintainer Communityhttps://maintainers.github.com
Acceleratorhttps://github.com/open-source/accelerator
GitHub Starshttps://stars.github.com
Archive Programhttps://archiveprogram.github.com
Topicshttps://github.com/topics
Trendinghttps://github.com/trending
Collectionshttps://github.com/collections
Enterprise platformAI-powered developer platformhttps://github.com/enterprise
GitHub Advanced SecurityEnterprise-grade security featureshttps://github.com/security/advanced-security
Copilot for BusinessEnterprise-grade AI featureshttps://github.com/features/copilot/copilot-business
Premium SupportEnterprise-grade 24/7 supporthttps://github.com/enterprise/premium-support
Pricinghttps://github.com/pricing
Search syntax tipshttps://docs.github.com/search-github/github-code-search/understanding-github-code-search-syntax
documentationhttps://docs.github.com/search-github/github-code-search/understanding-github-code-search-syntax
Sign in https://github.com/login?return_to=https%3A%2F%2Fgithub.com%2Fmodelcontextprotocol%2Fpython-sdk%2Fissues%2F396
Sign up https://github.com/signup?ref_cta=Sign+up&ref_loc=header+logged+out&ref_page=%2F%3Cuser-name%3E%2F%3Crepo-name%3E%2Fvoltron%2Fissues_fragments%2Fissue_layout&source=header-repo&source_repo=modelcontextprotocol%2Fpython-sdk
Reloadhttps://github.com/modelcontextprotocol/python-sdk/issues/396
Reloadhttps://github.com/modelcontextprotocol/python-sdk/issues/396
Reloadhttps://github.com/modelcontextprotocol/python-sdk/issues/396
Please reload this pagehttps://github.com/modelcontextprotocol/python-sdk/issues/396
modelcontextprotocol https://github.com/modelcontextprotocol
python-sdkhttps://github.com/modelcontextprotocol/python-sdk
Notifications https://github.com/login?return_to=%2Fmodelcontextprotocol%2Fpython-sdk
Fork 3.7k https://github.com/login?return_to=%2Fmodelcontextprotocol%2Fpython-sdk
Star 23.6k https://github.com/login?return_to=%2Fmodelcontextprotocol%2Fpython-sdk
Code https://github.com/modelcontextprotocol/python-sdk
Issues 256 https://github.com/modelcontextprotocol/python-sdk/issues
Pull requests 303 https://github.com/modelcontextprotocol/python-sdk/pulls
Actions https://github.com/modelcontextprotocol/python-sdk/actions
Projects https://github.com/modelcontextprotocol/python-sdk/projects
Models https://github.com/modelcontextprotocol/python-sdk/models
Security and quality 6 https://github.com/modelcontextprotocol/python-sdk/security
Insights https://github.com/modelcontextprotocol/python-sdk/pulse
Code https://github.com/modelcontextprotocol/python-sdk
Issues https://github.com/modelcontextprotocol/python-sdk/issues
Pull requests https://github.com/modelcontextprotocol/python-sdk/pulls
Actions https://github.com/modelcontextprotocol/python-sdk/actions
Projects https://github.com/modelcontextprotocol/python-sdk/projects
Models https://github.com/modelcontextprotocol/python-sdk/models
Security and quality https://github.com/modelcontextprotocol/python-sdk/security
Insights https://github.com/modelcontextprotocol/python-sdk/pulse
MCP Server: Inconsistent Exception Handling in @app.call_tool and Client Undetected Server Termination via Stdiohttps://github.com/modelcontextprotocol/python-sdk/issues/396#top
P3Nice to haves, rare edge caseshttps://github.com/modelcontextprotocol/python-sdk/issues?q=state%3Aopen%20label%3A%22P3%22
bugSomething isn't workinghttps://github.com/modelcontextprotocol/python-sdk/issues?q=state%3Aopen%20label%3A%22bug%22
ready for workEnough information for someone to start working onhttps://github.com/modelcontextprotocol/python-sdk/issues?q=state%3Aopen%20label%3A%22ready%20for%20work%22
https://github.com/omasoud
omasoudhttps://github.com/omasoud
on Mar 31, 2025https://github.com/modelcontextprotocol/python-sdk/issues/396#issue-2959475133
P3Nice to haves, rare edge caseshttps://github.com/modelcontextprotocol/python-sdk/issues?q=state%3Aopen%20label%3A%22P3%22
bugSomething isn't workinghttps://github.com/modelcontextprotocol/python-sdk/issues?q=state%3Aopen%20label%3A%22bug%22
ready for workEnough information for someone to start working onhttps://github.com/modelcontextprotocol/python-sdk/issues?q=state%3Aopen%20label%3A%22ready%20for%20work%22
https://github.com
Termshttps://docs.github.com/site-policy/github-terms/github-terms-of-service
Privacyhttps://docs.github.com/site-policy/privacy-policies/github-privacy-statement
Securityhttps://github.com/security
Statushttps://www.githubstatus.com/
Communityhttps://github.community/
Docshttps://docs.github.com/
Contacthttps://support.github.com?tags=dotcom-footer

Viewport: width=device-width


URLs of crawlers that visited me.