Skip to content

Commit 8f27296

Browse files
john-walkoeclaude
andcommitted
Fix async lifecycle bug across all 12 PTAB MCP tools
Resolves "cannot schedule new futures after interpreter shutdown" errors in async contexts by implementing lazy API client initialization pattern. Changes: - Add get_api_client() helper function for lazy initialization - Update 12 tools with api_client initialization checks: * 9 search tools (trials, appeals, interferences - minimal/balanced/complete) * 3 document tools (get_documents, get_document_download, get_document_content) - Add enhanced RuntimeError exception handling to 3 document tools - Enhance PyPDF2 error messaging with LLM guidance for scanned PDFs Testing: All 25 integration tests passed in Claude Desktop Pattern: Matches PFW MCP fix (commits 38e807c and b88d74a) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent f2cefcb commit 8f27296

File tree

1 file changed

+142
-7
lines changed

1 file changed

+142
-7
lines changed

src/ptab_mcp/main.py

Lines changed: 142 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,24 @@
112112

113113
api_client = PTABClient(api_key=settings.uspto_api_key)
114114

115+
116+
def get_api_client() -> PTABClient:
117+
"""
118+
Lazily initialize and return the API client.
119+
120+
This ensures the client is properly initialized even in complex async contexts
121+
where the event loop lifecycle may vary between MCP clients.
122+
123+
Returns:
124+
PTABClient instance
125+
"""
126+
global api_client
127+
if api_client is None:
128+
logger.info("Initializing PTAB API client")
129+
api_client = PTABClient(api_key=settings.uspto_api_key)
130+
return api_client
131+
132+
115133
# Initialize field manager with config path
116134
config_path = Path(__file__).parent.parent.parent / "field_configs.yaml"
117135
field_manager = FieldManager(config_path=config_path)
@@ -251,6 +269,12 @@ async def search_trials_minimal(
251269
"count": 2, "results": [...], "context_reduction": {...}}
252270
"""
253271
try:
272+
# Ensure API client is initialized (critical fix for async lifecycle issues)
273+
global api_client
274+
if api_client is None:
275+
logger.info("Initializing API client for trial search")
276+
api_client = get_api_client()
277+
254278
# Validate inputs
255279
if trial_number:
256280
trial_number = validate_trial_number(trial_number)
@@ -434,6 +458,12 @@ async def search_trials_balanced(
434458
JSON string with comprehensive trial data (balanced or custom field set)
435459
"""
436460
try:
461+
# Ensure API client is initialized (critical fix for async lifecycle issues)
462+
global api_client
463+
if api_client is None:
464+
logger.info("Initializing API client for trial search")
465+
api_client = get_api_client()
466+
437467
# Validate inputs
438468
if trial_number:
439469
trial_number = validate_trial_number(trial_number)
@@ -611,6 +641,12 @@ async def search_trials_complete(
611641
JSON string with complete trial data (all fields or custom field set)
612642
"""
613643
try:
644+
# Ensure API client is initialized (critical fix for async lifecycle issues)
645+
global api_client
646+
if api_client is None:
647+
logger.info("Initializing API client for trial search")
648+
api_client = get_api_client()
649+
614650
# Validate inputs (same as minimal)
615651
if trial_number:
616652
trial_number = validate_trial_number(trial_number)
@@ -836,6 +872,12 @@ async def ptab_get_documents(
836872
]}
837873
"""
838874
try:
875+
# Ensure API client is initialized (critical fix for async lifecycle issues)
876+
global api_client
877+
if api_client is None:
878+
logger.info("Initializing API client for document operations")
879+
api_client = get_api_client()
880+
839881
# Validate limit
840882
if limit < 1 or limit > 200:
841883
raise ValueError("Limit must be between 1 and 200")
@@ -980,6 +1022,18 @@ async def ptab_get_documents(
9801022

9811023
except ValueError as e:
9821024
return format_error_response(str(e), "VALIDATION_ERROR")
1025+
except RuntimeError as e:
1026+
# Catch async lifecycle errors specifically
1027+
error_msg = str(e)
1028+
if "cannot schedule new futures" in error_msg or "interpreter shutdown" in error_msg:
1029+
logger.error(f"Async lifecycle error in ptab_get_documents: {error_msg}")
1030+
return json.dumps({
1031+
"error": True,
1032+
"message": "Operation failed due to async runtime issue. Try restarting the MCP server.",
1033+
"technical_details": error_msg
1034+
}, indent=2)
1035+
else:
1036+
raise # Re-raise other RuntimeErrors
9831037
except Exception as e:
9841038
logger.error(f"Error in ptab_get_documents: {str(e)}")
9851039
return format_error_response(str(e), "API_ERROR")
@@ -1101,6 +1155,12 @@ async def ptab_get_document_download(
11011155
}
11021156
"""
11031157
try:
1158+
# Ensure API client is initialized (critical fix for async lifecycle issues)
1159+
global api_client
1160+
if api_client is None:
1161+
logger.info("Initializing API client for document download")
1162+
api_client = get_api_client()
1163+
11041164
# Validate inputs
11051165
identifier_type = validate_identifier_type(identifier_type)
11061166

@@ -1345,6 +1405,18 @@ async def ptab_get_document_download(
13451405

13461406
except ValueError as e:
13471407
return format_error_response(str(e), "VALIDATION_ERROR")
1408+
except RuntimeError as e:
1409+
# Catch async lifecycle errors specifically
1410+
error_msg = str(e)
1411+
if "cannot schedule new futures" in error_msg or "interpreter shutdown" in error_msg:
1412+
logger.error(f"Async lifecycle error in ptab_get_document_download: {error_msg}")
1413+
return json.dumps({
1414+
"error": True,
1415+
"message": "Operation failed due to async runtime issue. Try restarting the MCP server.",
1416+
"technical_details": error_msg
1417+
}, indent=2)
1418+
else:
1419+
raise # Re-raise other RuntimeErrors
13481420
except Exception as e:
13491421
logger.error(f"Error in ptab_get_document_download: {str(e)}")
13501422
return format_error_response(str(e), "API_ERROR")
@@ -1437,6 +1509,12 @@ async def ptab_get_document_content(
14371509
}
14381510
"""
14391511
try:
1512+
# Ensure API client is initialized (critical fix for async lifecycle issues)
1513+
global api_client
1514+
if api_client is None:
1515+
logger.info("Initializing API client for document content extraction")
1516+
api_client = get_api_client()
1517+
14401518
# Validate inputs
14411519
identifier_type = validate_identifier_type(identifier_type)
14421520

@@ -1592,14 +1670,23 @@ async def ptab_get_document_content(
15921670
error_msg = ocr_result.get("message", "Unknown OCR error")
15931671
logger.error(f"Mistral OCR extraction failed: {error_msg}")
15941672

1595-
# Return fallback with PyPDF2 result if available
1673+
# Return enhanced error with LLM guidance when both extraction methods fail
15961674
if not extracted_text:
1597-
extracted_text = (
1598-
f"[Mistral OCR extraction failed: {error_msg}]\n\n"
1599-
f"PyPDF2 result: {extracted_text if extracted_text else '(no text extracted)'}\n\n"
1600-
"Note: For scanned PDFs or documents with poor PyPDF2 extraction, "
1601-
"configure MISTRAL_API_KEY for improved OCR capabilities."
1602-
)
1675+
return json.dumps({
1676+
"document_id": document_id,
1677+
"identifier": identifier,
1678+
"text": "",
1679+
"extraction_method": "PyPDF2 (insufficient)",
1680+
"error": "Document appears to be scanned/image-based. PyPDF2 could not extract meaningful text.",
1681+
"mistral_api_key_missing": not ocr_service.mistral_api_key,
1682+
"llm_guidance": {
1683+
"explain_to_user": "Many USPTO PTAB documents are scanned images rather than text-based PDFs. "
1684+
"PyPDF2 can only extract text from text-based PDFs - it cannot read scanned images.",
1685+
"recommended_solution": "Configure Mistral API for OCR capability (~$0.001/page, with free tier available)",
1686+
"free_tier_info": "Mistral offers a generous free tier - sign up at https://console.mistral.ai/",
1687+
"setup_instructions": "Set MISTRAL_API_KEY environment variable after obtaining key from Mistral console"
1688+
}
1689+
}, indent=2)
16031690
ocr_cost_usd = 0.00
16041691

16051692
# Return result
@@ -1619,6 +1706,18 @@ async def ptab_get_document_content(
16191706

16201707
except ValueError as e:
16211708
return format_error_response(str(e), "VALIDATION_ERROR")
1709+
except RuntimeError as e:
1710+
# Catch async lifecycle errors specifically
1711+
error_msg = str(e)
1712+
if "cannot schedule new futures" in error_msg or "interpreter shutdown" in error_msg:
1713+
logger.error(f"Async lifecycle error in ptab_get_document_content: {error_msg}")
1714+
return json.dumps({
1715+
"error": True,
1716+
"message": "Operation failed due to async runtime issue. Try restarting the MCP server.",
1717+
"technical_details": error_msg
1718+
}, indent=2)
1719+
else:
1720+
raise # Re-raise other RuntimeErrors
16221721
except Exception as e:
16231722
logger.error(f"Error in ptab_get_document_content: {str(e)}")
16241723
return format_error_response(str(e), "API_ERROR")
@@ -1710,6 +1809,12 @@ async def search_appeals_minimal(
17101809
"count": 5, "results": [...], "context_reduction": {...}}
17111810
"""
17121811
try:
1812+
# Ensure API client is initialized (critical fix for async lifecycle issues)
1813+
global api_client
1814+
if api_client is None:
1815+
logger.info("Initializing API client for appeal search")
1816+
api_client = get_api_client()
1817+
17131818
# Validate inputs
17141819
if appeal_number:
17151820
appeal_number = validate_appeal_number(appeal_number)
@@ -1880,6 +1985,12 @@ async def search_appeals_balanced(
18801985
JSON string with comprehensive appeal data (balanced or custom field set)
18811986
"""
18821987
try:
1988+
# Ensure API client is initialized (critical fix for async lifecycle issues)
1989+
global api_client
1990+
if api_client is None:
1991+
logger.info("Initializing API client for appeal search")
1992+
api_client = get_api_client()
1993+
18831994
# Validate inputs
18841995
if appeal_number:
18851996
appeal_number = validate_appeal_number(appeal_number)
@@ -2052,6 +2163,12 @@ async def search_appeals_complete(
20522163
JSON string with complete appeal data (all fields or custom field set)
20532164
"""
20542165
try:
2166+
# Ensure API client is initialized (critical fix for async lifecycle issues)
2167+
global api_client
2168+
if api_client is None:
2169+
logger.info("Initializing API client for appeal search")
2170+
api_client = get_api_client()
2171+
20552172
# Validate inputs
20562173
if appeal_number:
20572174
appeal_number = validate_appeal_number(appeal_number)
@@ -2228,6 +2345,12 @@ async def search_interferences_minimal(
22282345
"count": 2, "results": [...], "context_reduction": {...}}
22292346
"""
22302347
try:
2348+
# Ensure API client is initialized (critical fix for async lifecycle issues)
2349+
global api_client
2350+
if api_client is None:
2351+
logger.info("Initializing API client for interference search")
2352+
api_client = get_api_client()
2353+
22312354
# Validate inputs
22322355
if interference_number:
22332356
interference_number = validate_interference_number(interference_number)
@@ -2389,6 +2512,12 @@ async def search_interferences_balanced(
23892512
JSON string with comprehensive interference data (balanced or custom field set)
23902513
"""
23912514
try:
2515+
# Ensure API client is initialized (critical fix for async lifecycle issues)
2516+
global api_client
2517+
if api_client is None:
2518+
logger.info("Initializing API client for interference search")
2519+
api_client = get_api_client()
2520+
23922521
# Validate inputs
23932522
if interference_number:
23942523
interference_number = validate_interference_number(interference_number)
@@ -2560,6 +2689,12 @@ async def search_interferences_complete(
25602689
JSON string with complete interference data (all fields or custom field set)
25612690
"""
25622691
try:
2692+
# Ensure API client is initialized (critical fix for async lifecycle issues)
2693+
global api_client
2694+
if api_client is None:
2695+
logger.info("Initializing API client for interference search")
2696+
api_client = get_api_client()
2697+
25632698
# Validate inputs
25642699
if interference_number:
25652700
interference_number = validate_interference_number(interference_number)

0 commit comments

Comments
 (0)