|
| 1 | +""" |
| 2 | +Demo script for PR #1948: MCP HTTP error handling |
| 3 | +
|
| 4 | +This script demonstrates how MCP tools now handle upstream HTTP errors gracefully |
| 5 | +instead of crashing the agent run. |
| 6 | +
|
| 7 | +Prerequisites: |
| 8 | +- Python 3.10+ (required by MCP package) |
| 9 | +- Set OPENAI_API_KEY environment variable |
| 10 | +
|
| 11 | +The script uses a mock MCP server that simulates HTTP errors. |
| 12 | +""" |
| 13 | + |
| 14 | +import asyncio |
| 15 | +import json |
| 16 | +from typing import Any |
| 17 | + |
| 18 | +from agents import Agent, Runner, function_tool |
| 19 | + |
| 20 | + |
| 21 | +# Mock MCP server that simulates HTTP errors |
| 22 | +class MockMCPServerWithErrors: |
| 23 | + """A mock MCP server that simulates various HTTP error scenarios.""" |
| 24 | + |
| 25 | + def __init__(self): |
| 26 | + self.call_count = 0 |
| 27 | + |
| 28 | + async def call_tool(self, tool_name: str, arguments: dict[str, Any]): |
| 29 | + """Simulate MCP tool calls with different error scenarios.""" |
| 30 | + self.call_count += 1 |
| 31 | + |
| 32 | + # Simulate different error scenarios based on query |
| 33 | + query = arguments.get("query", "") |
| 34 | + |
| 35 | + if "invalid" in query.lower(): |
| 36 | + # Simulate 422 Validation Error |
| 37 | + from mcp.shared.exceptions import McpError |
| 38 | + |
| 39 | + raise McpError("GET https://api.example.com/search: 422 Validation Error") |
| 40 | + |
| 41 | + if "notfound" in query.lower(): |
| 42 | + # Simulate 404 Not Found |
| 43 | + from mcp.shared.exceptions import McpError |
| 44 | + |
| 45 | + raise McpError("GET https://api.example.com/search: 404 Not Found") |
| 46 | + |
| 47 | + if "servererror" in query.lower(): |
| 48 | + # Simulate 500 Internal Server Error |
| 49 | + from mcp.shared.exceptions import McpError |
| 50 | + |
| 51 | + raise McpError("GET https://api.example.com/search: 500 Internal Server Error") |
| 52 | + |
| 53 | + # Successful case |
| 54 | + return type( |
| 55 | + "Result", |
| 56 | + (), |
| 57 | + { |
| 58 | + "content": [ |
| 59 | + type( |
| 60 | + "Content", |
| 61 | + (), |
| 62 | + { |
| 63 | + "model_dump_json": lambda: json.dumps( |
| 64 | + {"results": f"Search results for: {query}"} |
| 65 | + ) |
| 66 | + }, |
| 67 | + )() |
| 68 | + ], |
| 69 | + "structuredContent": None, |
| 70 | + }, |
| 71 | + )() |
| 72 | + |
| 73 | + |
| 74 | +# Create a search tool using the mock MCP server |
| 75 | +mock_server = MockMCPServerWithErrors() |
| 76 | + |
| 77 | + |
| 78 | +@function_tool |
| 79 | +async def search(query: str) -> str: |
| 80 | + """Search for information using an MCP-backed API. |
| 81 | +
|
| 82 | + Args: |
| 83 | + query: The search query |
| 84 | +
|
| 85 | + Returns: |
| 86 | + Search results or error message |
| 87 | + """ |
| 88 | + # This simulates how MCPUtil.invoke_mcp_tool works |
| 89 | + from mcp.shared.exceptions import McpError |
| 90 | + |
| 91 | + try: |
| 92 | + result = await mock_server.call_tool("search", {"query": query}) |
| 93 | + return result.content[0].model_dump_json() |
| 94 | + except McpError as e: |
| 95 | + # After PR #1948: Return structured error instead of crashing |
| 96 | + return json.dumps( |
| 97 | + {"error": {"message": str(e), "tool": "search", "type": "upstream_error"}} |
| 98 | + ) |
| 99 | + except Exception as e: |
| 100 | + # Programming errors still raise |
| 101 | + raise |
| 102 | + |
| 103 | + |
| 104 | +async def main(): |
| 105 | + """Demonstrate MCP HTTP error handling.""" |
| 106 | + print("=" * 70) |
| 107 | + print("MCP HTTP Error Handling Demo (PR #1948)") |
| 108 | + print("=" * 70) |
| 109 | + print() |
| 110 | + |
| 111 | + agent = Agent( |
| 112 | + name="SearchAgent", |
| 113 | + model="gpt-4o-mini", |
| 114 | + instructions="You are a helpful search assistant. " |
| 115 | + "When search fails, explain the error to the user kindly.", |
| 116 | + tools=[search], |
| 117 | + ) |
| 118 | + |
| 119 | + # Test Case 1: Successful search |
| 120 | + print("\n" + "─" * 70) |
| 121 | + print("Test 1: Successful Search") |
| 122 | + print("─" * 70) |
| 123 | + result1 = await Runner.run(agent, input="Search for: Python programming") |
| 124 | + print(f"✅ Agent Response: {result1.final_output}") |
| 125 | + |
| 126 | + # Test Case 2: 422 Validation Error (invalid query) |
| 127 | + print("\n" + "─" * 70) |
| 128 | + print("Test 2: HTTP 422 - Invalid Query") |
| 129 | + print("─" * 70) |
| 130 | + result2 = await Runner.run(agent, input="Search for: invalid query") |
| 131 | + print(f"✅ Agent Response: {result2.final_output}") |
| 132 | + print(" (Notice: Agent handled the error gracefully, run didn't crash)") |
| 133 | + |
| 134 | + # Test Case 3: 404 Not Found |
| 135 | + print("\n" + "─" * 70) |
| 136 | + print("Test 3: HTTP 404 - Not Found") |
| 137 | + print("─" * 70) |
| 138 | + result3 = await Runner.run(agent, input="Search for: notfound resource") |
| 139 | + print(f"✅ Agent Response: {result3.final_output}") |
| 140 | + |
| 141 | + # Test Case 4: 500 Internal Server Error |
| 142 | + print("\n" + "─" * 70) |
| 143 | + print("Test 4: HTTP 500 - Server Error") |
| 144 | + print("─" * 70) |
| 145 | + result4 = await Runner.run(agent, input="Search for: servererror test") |
| 146 | + print(f"✅ Agent Response: {result4.final_output}") |
| 147 | + |
| 148 | + print("\n" + "=" * 70) |
| 149 | + print("Summary") |
| 150 | + print("=" * 70) |
| 151 | + print(f"Total MCP tool calls: {mock_server.call_count}") |
| 152 | + print("✅ All tests completed successfully") |
| 153 | + print("✅ Agent run didn't crash on HTTP errors") |
| 154 | + print("✅ Agent gracefully handled all error scenarios") |
| 155 | + print() |
| 156 | + print("Before PR #1948:") |
| 157 | + print(" ❌ Any HTTP error → AgentsException → Agent run crashes") |
| 158 | + print() |
| 159 | + print("After PR #1948:") |
| 160 | + print(" ✅ HTTP errors → Structured error response → Agent continues") |
| 161 | + print(" ✅ Agent can inform user, retry, or try alternatives") |
| 162 | + print("=" * 70) |
| 163 | + |
| 164 | + |
| 165 | +if __name__ == "__main__": |
| 166 | + try: |
| 167 | + asyncio.run(main()) |
| 168 | + except ImportError as e: |
| 169 | + if "mcp" in str(e): |
| 170 | + print("⚠️ This demo requires Python 3.10+ (MCP package dependency)") |
| 171 | + print(" Please upgrade Python or test with the unit tests instead:") |
| 172 | + print(" pytest tests/mcp/test_issue_879_http_error_handling.py") |
| 173 | + else: |
| 174 | + raise |
0 commit comments