Skip to content

Commit 4068b70

Browse files
committed
fix: harden all tools against crashes and improve search_users and pagination
- search_users: remove N+1 load() calls, read directly from _data dict to avoid PararamModelNotLoadedError, add pm_chat_id to response - get_chat_messages: implement before_message_id pagination support - upload_file_to_chat: fallback to local file metadata when API response lacks name/size fields - Add catch-all except Exception handlers to all 12 tool functions to prevent MCP server crashes on unexpected errors - Make UserInfo.active, organizations optional (not available from search API) - Make UploadFilePayload.file_id, url optional (not always returned by API)
1 parent 36f81b0 commit 4068b70

4 files changed

Lines changed: 121 additions & 27 deletions

File tree

src/pararam_nexus_mcp/models.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,10 @@ class BuildConversationThreadResponse(BaseModel):
179179
class UploadFilePayload(BaseModel):
180180
"""Payload for upload_file_to_chat response."""
181181

182-
file_id: str = Field(..., description='Unique identifier for the file')
182+
file_id: str | None = Field(None, description='Unique identifier for the file')
183183
filename: str = Field(..., description='Name of the uploaded file')
184184
size: int = Field(..., description='File size in bytes')
185-
url: str = Field(..., description='URL to access the file')
185+
url: str | None = Field(None, description='URL to access the file')
186186
chat_id: str = Field(..., description='ID of the chat')
187187

188188

@@ -276,9 +276,10 @@ class UserInfo(BaseModel):
276276
id: int = Field(..., description='User ID')
277277
name: str = Field(..., description="User's display name")
278278
unique_name: str = Field(..., description="User's unique username")
279-
active: bool = Field(..., description='Whether user is active')
279+
active: bool | None = Field(None, description='Whether user is active')
280280
is_bot: bool = Field(..., description='Whether user is a bot')
281-
organizations: list[int] = Field(..., description='List of organization IDs')
281+
pm_chat_id: int | None = Field(None, description='PM chat ID for direct messaging this user')
282+
organizations: list[int] | None = Field(None, description='List of organization IDs')
282283

283284

284285
class SearchUsersPayload(BaseModel):

src/pararam_nexus_mcp/tools/chats.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,10 @@ async def search_chats(
114114
message='Network error occurred',
115115
error=f'Network error: {e!s}',
116116
)
117+
except Exception as e:
118+
# Top-level handler to prevent MCP server crash on unexpected errors
119+
logger.error('Unexpected error while searching chats: %s', e, exc_info=True)
120+
return error_response(
121+
message='Unexpected error occurred',
122+
error=f'Unexpected error: {e!s}',
123+
)

src/pararam_nexus_mcp/tools/posts.py

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,13 @@ async def search_messages(
345345
message='Network error occurred',
346346
error=f'Network error: {e!s}',
347347
)
348+
except Exception as e:
349+
# Top-level handler to prevent MCP server crash on unexpected errors
350+
logger.error('Unexpected error while searching messages: %s', e, exc_info=True)
351+
return error_response(
352+
message='Unexpected error occurred',
353+
error=f'Unexpected error: {e!s}',
354+
)
348355

349356
@mcp.tool()
350357
async def get_chat_messages(
@@ -358,15 +365,18 @@ async def get_chat_messages(
358365
Args:
359366
chat_id: ID of the chat to get messages from
360367
limit: Maximum number of messages to return (default: 50)
361-
before_message_id: Get messages before this message ID (for pagination, currently not used)
368+
before_message_id: Get messages before this message ID (for pagination).
369+
If provided, returns messages older than this ID.
362370
363371
Returns:
364372
ToolResponse with GetChatMessagesPayload containing chat messages including sender, text, and timestamp
365373
"""
366374
try:
367375
client = await get_client()
368376

369-
logger.info(f'Getting messages from chat {chat_id}, limit: {limit}')
377+
logger.info(
378+
f'Getting messages from chat {chat_id}, limit: {limit}, before_message_id: {before_message_id}'
379+
)
370380

371381
# Get chat by ID
372382
chat = await client.client.get_chat_by_id(int(chat_id))
@@ -376,9 +386,14 @@ async def get_chat_messages(
376386
error='Chat not found',
377387
)
378388

379-
# Get recent messages using load_posts (negative indices mean from the end)
380-
# -limit to -1 means last 'limit' messages
381-
messages = await chat.load_posts(start_post_no=-limit, end_post_no=-1)
389+
# Load posts: if before_message_id is set, load range ending before it
390+
if before_message_id:
391+
end_no = int(before_message_id) - 1
392+
start_no = max(end_no - limit + 1, 1)
393+
messages = await chat.load_posts(start_post_no=start_no, end_post_no=end_no)
394+
else:
395+
# Get last 'limit' messages
396+
messages = await chat.load_posts(start_post_no=-limit, end_post_no=-1)
382397

383398
if not messages:
384399
return error_response(
@@ -459,6 +474,13 @@ async def get_chat_messages(
459474
message='Network error occurred',
460475
error=f'Network error: {e!s}',
461476
)
477+
except Exception as e:
478+
# Top-level handler to prevent MCP server crash on unexpected errors
479+
logger.error('Unexpected error while getting chat messages: %s', e, exc_info=True)
480+
return error_response(
481+
message='Unexpected error occurred',
482+
error=f'Unexpected error: {e!s}',
483+
)
462484

463485
@mcp.tool()
464486
async def send_message(
@@ -539,6 +561,13 @@ async def send_message(
539561
message='Network error occurred',
540562
error=f'Network error: {e!s}',
541563
)
564+
except Exception as e:
565+
# Top-level handler to prevent MCP server crash on unexpected errors
566+
logger.error('Unexpected error while sending message: %s', e, exc_info=True)
567+
return error_response(
568+
message='Unexpected error occurred',
569+
error=f'Unexpected error: {e!s}',
570+
)
542571

543572
@mcp.tool()
544573
async def build_conversation_thread(
@@ -697,6 +726,13 @@ def collect_thread_posts(post_no: int) -> None:
697726
message='Network error occurred',
698727
error=f'Network error: {e!s}',
699728
)
729+
except Exception as e:
730+
# Top-level handler to prevent MCP server crash on unexpected errors
731+
logger.error('Unexpected error while building conversation thread: %s', e, exc_info=True)
732+
return error_response(
733+
message='Unexpected error occurred',
734+
error=f'Unexpected error: {e!s}',
735+
)
700736

701737
@mcp.tool()
702738
async def upload_file_to_chat(
@@ -775,17 +811,22 @@ async def upload_file_to_chat(
775811
reply_no = int(reply_to_message_id) if reply_to_message_id else None
776812
uploaded_file = await chat.upload_file(upload_path, reply_no=reply_no)
777813

814+
# Fallback to local file info when API response lacks fields
815+
actual_path = Path(upload_path)
816+
file_name = uploaded_file.name or actual_path.name
817+
file_size = uploaded_file.size or actual_path.stat().st_size
818+
778819
chat_name = chat.title or 'Unknown'
779820
message_text = (
780-
f"File '{uploaded_file.name}' uploaded successfully to chat '{chat_name}' ({uploaded_file.size} bytes)"
821+
f"File '{file_name}' uploaded successfully to chat '{chat_name}' ({file_size} bytes)"
781822
)
782823

783824
return success_response(
784825
message=message_text,
785826
payload=UploadFilePayload(
786827
file_id=uploaded_file.guid,
787-
filename=uploaded_file.name,
788-
size=uploaded_file.size,
828+
filename=file_name,
829+
size=file_size,
789830
url=uploaded_file.url,
790831
chat_id=chat_id,
791832
),
@@ -827,6 +868,13 @@ async def upload_file_to_chat(
827868
message='Network error occurred',
828869
error=f'Network error: {e!s}',
829870
)
871+
except Exception as e:
872+
# Top-level handler to prevent MCP server crash on unexpected errors
873+
logger.error('Unexpected error while uploading file: %s', e, exc_info=True)
874+
return error_response(
875+
message='Unexpected error occurred',
876+
error=f'Unexpected error: {e!s}',
877+
)
830878
finally:
831879
# Clean up temporary file if created
832880
if temp_file_path and temp_file_path.exists():
@@ -961,6 +1009,13 @@ async def get_message_from_url(
9611009
message='Network error occurred',
9621010
error=f'Network error: {e!s}',
9631011
)
1012+
except Exception as e:
1013+
# Top-level handler to prevent MCP server crash on unexpected errors
1014+
logger.error('Unexpected error while getting message from URL: %s', e, exc_info=True)
1015+
return error_response(
1016+
message='Unexpected error occurred',
1017+
error=f'Unexpected error: {e!s}',
1018+
)
9641019

9651020
@mcp.tool()
9661021
async def get_post_attachments(
@@ -1124,6 +1179,13 @@ async def get_post_attachments(
11241179
message='Network error occurred',
11251180
error=f'Network error: {e!s}',
11261181
)
1182+
except Exception as e:
1183+
# Top-level handler to prevent MCP server crash on unexpected errors
1184+
logger.error('Unexpected error while getting post attachments: %s', e, exc_info=True)
1185+
return error_response(
1186+
message='Unexpected error occurred',
1187+
error=f'Unexpected error: {e!s}',
1188+
)
11271189

11281190
@mcp.tool()
11291191
async def download_post_attachment(
@@ -1422,3 +1484,10 @@ async def download_post_attachment(
14221484
size=0,
14231485
mime_type='',
14241486
)
1487+
except Exception as e:
1488+
# Top-level handler to prevent MCP server crash on unexpected errors
1489+
logger.error('Unexpected error while downloading attachment: %s', e, exc_info=True)
1490+
return error_response(
1491+
message='Unexpected error occurred',
1492+
error=f'Unexpected error: {e!s}',
1493+
)

src/pararam_nexus_mcp/tools/users.py

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,22 +65,18 @@ async def search_users(
6565
# Apply limit
6666
users = users[:limit]
6767

68-
# Format results
69-
formatted_users = []
70-
for user in users:
71-
# Load user data
72-
await user.load()
73-
74-
formatted_users.append(
75-
UserInfo(
76-
id=user.id,
77-
name=user.name,
78-
unique_name=user.unique_name,
79-
active=user.active,
80-
is_bot=user.is_bot,
81-
organizations=user.organizations,
82-
)
68+
# Format results — use _data dict directly to avoid PararamModelNotLoadedError
69+
# when optional fields are missing from search API response
70+
formatted_users = [
71+
UserInfo(
72+
id=user._data.get('id', 0),
73+
name=user._data.get('name', ''),
74+
unique_name=user._data.get('unique_name', ''),
75+
is_bot=user._data.get('is_bot', False),
76+
pm_chat_id=user._data.get('pm_thread_id'),
8377
)
78+
for user in users
79+
]
8480

8581
message_text = f"Found {len(formatted_users)} users matching '{query}'"
8682

@@ -117,6 +113,13 @@ async def search_users(
117113
message='Network error occurred',
118114
error=f'Network error: {e!s}',
119115
)
116+
except Exception as e:
117+
# Top-level handler to prevent MCP server crash on unexpected errors
118+
logger.error('Unexpected error while searching users: %s', e, exc_info=True)
119+
return error_response(
120+
message='Unexpected error occurred',
121+
error=f'Unexpected error: {e!s}',
122+
)
120123

121124
@mcp.tool()
122125
async def get_user_info(
@@ -194,6 +197,13 @@ async def get_user_info(
194197
message='Network error occurred',
195198
error=f'Network error: {e!s}',
196199
)
200+
except Exception as e:
201+
# Top-level handler to prevent MCP server crash on unexpected errors
202+
logger.error('Unexpected error while getting user info: %s', e, exc_info=True)
203+
return error_response(
204+
message='Unexpected error occurred',
205+
error=f'Unexpected error: {e!s}',
206+
)
197207

198208
@mcp.tool()
199209
async def get_user_team_status(
@@ -294,3 +304,10 @@ async def get_user_team_status(
294304
message='Network error occurred',
295305
error=f'Network error: {e!s}',
296306
)
307+
except Exception as e:
308+
# Top-level handler to prevent MCP server crash on unexpected errors
309+
logger.error('Unexpected error while getting team status: %s', e, exc_info=True)
310+
return error_response(
311+
message='Unexpected error occurred',
312+
error=f'Unexpected error: {e!s}',
313+
)

0 commit comments

Comments
 (0)