| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
…protocol#348)\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…contextprotocol#1564)\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…xtprotocol#1933)\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
This PR aims to fix #1933 by preventing the stdio transport from closing the real process stdin/stdout when wrapper streams are closed, by duplicating the underlying file descriptors before wrapping.
Changes:
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| src/mcp/server/stdio.py | Duplicates stdio file descriptors before wrapping to prevent closing real process stdio. |
| src/mcp/server/mcpserver/server.py | Expands tuple handling for tool results (including optional is_error). |
| src/mcp/client/stdio.py | Treats BrokenResourceError similarly to ClosedResourceError during stdout read loop shutdown. |
| docs/migration.md | Adds migration documentation describing the new 3-tuple tool result behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Sorry, something went wrong.
| if not stdin: | ||
| stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace")) | ||
| # Duplicate the underlying file descriptors so closing the wrapper | ||
| # does not close the real process stdio (fixes issue #1933). | ||
| stdin_dup = os.fdopen(os.dup(sys.stdin.buffer.fileno()), 'rb') | ||
| stdin = anyio.wrap_file(TextIOWrapper(stdin_dup, encoding="utf-8", errors="replace")) | ||
| if not stdout: | ||
| stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8")) | ||
| stdout_dup = os.fdopen(os.dup(sys.stdout.buffer.fileno()), 'wb') | ||
| stdout = anyio.wrap_file(TextIOWrapper(stdout_dup, encoding="utf-8")) |
| if not stdin: | ||
| stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding="utf-8", errors="replace")) | ||
| # Duplicate the underlying file descriptors so closing the wrapper | ||
| # does not close the real process stdio (fixes issue #1933). | ||
| stdin_dup = os.fdopen(os.dup(sys.stdin.buffer.fileno()), 'rb') | ||
| stdin = anyio.wrap_file(TextIOWrapper(stdin_dup, encoding="utf-8", errors="replace")) | ||
| if not stdout: | ||
| stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding="utf-8")) | ||
| stdout_dup = os.fdopen(os.dup(sys.stdout.buffer.fileno()), 'wb') | ||
| stdout = anyio.wrap_file(TextIOWrapper(stdout_dup, encoding="utf-8")) |
| # Support either (unstructured_content, structured_content) or | ||
| # (unstructured_content, structured_content, is_error). The third element, | ||
| # if present, controls the CallToolResult.is_error flag. | ||
| if len(result) == 2: | ||
| unstructured_content, structured_content = result | ||
| is_error = False | ||
| elif len(result) == 3: | ||
| unstructured_content, structured_content, is_error = result | ||
| else: | ||
| # Fallback: treat as a sequence of content blocks | ||
| return CallToolResult(content=list(result)) | ||
| return CallToolResult( | ||
| content=list(unstructured_content), # type: ignore[arg-type] | ||
| structured_content=structured_content, # type: ignore[arg-type] | ||
| is_error=bool(is_error), |
| if isinstance(result, tuple): | ||
| # Support either (unstructured_content, structured_content) or | ||
| # (unstructured_content, structured_content, is_error). The third element, | ||
| # if present, controls the CallToolResult.is_error flag. | ||
| if len(result) == 2: | ||
| unstructured_content, structured_content = result | ||
| is_error = False | ||
| elif len(result) == 3: | ||
| unstructured_content, structured_content, is_error = result | ||
| else: | ||
| # Fallback: treat as a sequence of content blocks | ||
| return CallToolResult(content=list(result)) | ||
| return CallToolResult( | ||
| content=list(unstructured_content), # type: ignore[arg-type] | ||
| structured_content=structured_content, # type: ignore[arg-type] | ||
| is_error=bool(is_error), | ||
| ) |
| ## Need Help? | ||
|
|
||
| If you encounter issues during migration: | ||
|
|
||
| 1. Check the [API Reference](api/mcp/index.md) for updated method signatures | ||
| 2. Review the [examples](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples) for updated usage patterns | ||
| 3. Open an issue on [GitHub](https://github.com/modelcontextprotocol/python-sdk/issues) if you find a bug or need further assistance | ||
|
|
||
| ## Tool result: marking non-text results as errors | ||
| Tools may now return a 3-tuple: (unstructured_content, structured_content, is_error). | ||
| If the third element is True, the resulting CallToolResult sent to clients will have | ||
| is_error=True. This allows returning non-text content (images, audio) while still | ||
| indicating the tool execution failed or produced an error state. | ||
|
|
||
| Alternatively, tools may return a full `CallToolResult` instance directly to control | ||
| is_error and other fields explicitly. |
| if isinstance(result, tuple): | ||
| # Support either (unstructured_content, structured_content) or | ||
| # (unstructured_content, structured_content, is_error). The third element, | ||
| # if present, controls the CallToolResult.is_error flag. | ||
| if len(result) == 2: | ||
| unstructured_content, structured_content = result |
| Back | FazBrowse Home | New Git URL |
Fixes #1933 — duplicate underlying stdio file descriptors before wrapping so closing wrappers doesn't close the real process stdio. See issue: #1933