Skip to content

Commit e859562

Browse files
committed
work in progress
1 parent 8fe9710 commit e859562

File tree

5 files changed

+98
-43
lines changed

5 files changed

+98
-43
lines changed

src/mcp_server_uyuni/server.py

Lines changed: 78 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,7 @@ async def check_all_systems_for_updates(ctx: Context) -> List[Dict[str, Any]]:
568568
return systems_with_updates
569569

570570
@write_tool()
571-
async def schedule_apply_pending_updates_to_system(system_identifier: Union[str, int], ctx: Context, confirm: bool = False) -> str:
571+
async def schedule_apply_pending_updates_to_system(system_identifier: Union[str, int], ctx: Context, confirm: Union[bool, str] = False) -> str:
572572

573573
"""
574574
Checks for pending updates on a system, schedules all of them to be applied,
@@ -580,7 +580,11 @@ async def schedule_apply_pending_updates_to_system(system_identifier: Union[str,
580580
581581
Args:
582582
system_identifier: The unique identifier of the system. It can be the system name (e.g. "buildhost") or the system ID (e.g. 1000010000).
583-
confirm: False by default. Only set confirm to True if the user has explicetely confirmed. Ask the user for confirmation.
583+
confirm: User confirmation is required to execute this action. This parameter
584+
is `False` by default. To obtain the confirmation message that must
585+
be presented to the user, the model must first call the tool with
586+
`confirm=False`. If the user agrees, the model should call the tool
587+
a second time with `confirm=True`.
584588
585589
Returns:
586590
str: The action url if updates were successfully scheduled.
@@ -590,7 +594,10 @@ async def schedule_apply_pending_updates_to_system(system_identifier: Union[str,
590594
logger.info(log_string)
591595
await ctx.info(log_string)
592596

593-
if not confirm:
597+
# Coerce string "false" to boolean False for models that send strings
598+
is_confirmed = str(confirm).lower() == 'true'
599+
600+
if not is_confirmed:
594601
return f"CONFIRMATION REQUIRED: This will apply pending updates to the system {system_identifier}. Do you confirm?"
595602

596603
# 1. Use check_system_updates to get relevant errata
@@ -642,15 +649,19 @@ async def schedule_apply_pending_updates_to_system(system_identifier: Union[str,
642649
return ""
643650

644651
@write_tool()
645-
async def schedule_apply_specific_update(system_identifier: Union[str, int], errata_id: int, ctx: Context, confirm: bool = False) -> str:
652+
async def schedule_apply_specific_update(system_identifier: Union[str, int], errata_id: Union[str, int], ctx: Context, confirm: Union[bool, str] = False) -> str:
646653

647654
"""
648655
Schedules a specific update (erratum) to be applied to a system.
649656
650657
Args:
651658
system_identifier: The unique identifier of the system. It can be the system name (e.g. "buildhost") or the system ID (e.g. 1000010000).
652-
errata_id: The unique identifier of the erratum (also referred to as update ID) to be applied.
653-
confirm: False by default. Only set confirm to True if the user has explicetely confirmed. Ask the user for confirmation.
659+
errata_id: The unique identifier of the erratum (also referred to as update ID) to be applied. It must be an integer.
660+
confirm: User confirmation is required to execute this action. This parameter
661+
is `False` by default. To obtain the confirmation message that must
662+
be presented to the user, the model must first call the tool with
663+
`confirm=False`. If the user agrees, the model should call the tool
664+
a second time with `confirm=True`.
654665
655666
Returns:
656667
str: The action URL if the update was successfully scheduled.
@@ -659,41 +670,52 @@ async def schedule_apply_specific_update(system_identifier: Union[str, int], err
659670
log_string = f"Attempting to apply specific update (errata ID: {errata_id}) to system ID: {system_identifier}"
660671
logger.info(log_string)
661672
await ctx.info(log_string)
673+
674+
# Coerce string "false" to boolean False for models that send strings
675+
is_confirmed = str(confirm).lower() == 'true'
676+
677+
try:
678+
# Coerce errata_id to an integer, as some models might pass it as a string.
679+
errata_id_int = int(errata_id)
680+
except (ValueError, TypeError):
681+
return f"Invalid errata ID '{errata_id}'. The ID must be an integer."
682+
683+
662684
system_id = await _resolve_system_id(system_identifier)
663685
if not system_id:
664686
return "" # Helper function already logged the reason for failure.
665687

666688
print(f"Attempting to apply specific update (errata ID: {errata_id}) to system: {system_identifier}")
667689

668-
if not confirm:
690+
if not is_confirmed:
669691
return f"CONFIRMATION REQUIRED: This will apply specific update (errata ID: {errata_id}) to the system {system_identifier}. Do you confirm?"
670692

671693
async with httpx.AsyncClient(verify=UYUNI_MCP_SSL_VERIFY) as client:
672694
# The API expects a list of errata IDs, even if it's just one.
673-
payload = {"sid": int(system_id), "errataIds": [errata_id]}
695+
payload = {"sid": int(system_id), "errataIds": [errata_id_int]}
674696
api_result = await _call_uyuni_api(
675697
client=client,
676698
method="POST",
677699
api_path="/rhn/manager/api/system/scheduleApplyErrata",
678700
json_body=payload,
679-
error_context=f"scheduling specific update (errata ID: {errata_id}) for system {system_identifier}",
701+
error_context=f"scheduling specific update (errata ID: {errata_id_int}) for system {system_identifier}",
680702
default_on_error=None # Helper returns None on error
681703
)
682704

683705
if isinstance(api_result, list) and api_result and isinstance(api_result[0], int):
684706
action_id = api_result[0]
685-
success_message = f"Update (errata ID: {errata_id}) successfully scheduled for system {system_identifier}. Action URL: {UYUNI_SERVER}/rhn/schedule/ActionDetails.do?aid={action_id}"
707+
success_message = f"Update (errata ID: {errata_id_int}) successfully scheduled for system {system_identifier}. Action URL: {UYUNI_SERVER}/rhn/schedule/ActionDetails.do?aid={action_id}"
686708
print(success_message)
687709
return success_message
688710
# Some schedule APIs might return int directly in result (though scheduleApplyErrata usually returns a list)
689711
elif isinstance(api_result, int): # Defensive check
690712
action_id = api_result
691-
success_message = f"Update (errata ID: {errata_id}) successfully scheduled. Action URL: {UYUNI_SERVER}/rhn/schedule/ActionDetails.do?aid={action_id}"
713+
success_message = f"Update (errata ID: {errata_id_int}) successfully scheduled. Action URL: {UYUNI_SERVER}/rhn/schedule/ActionDetails.do?aid={action_id}"
692714
print(success_message)
693715
return success_message
694716
else:
695717
if api_result is not None: # Log if not None but also not expected format
696-
print(f"Failed to schedule specific update (errata ID: {errata_id}) for system {system_identifier} or unexpected API result format. Result: {api_result}")
718+
print(f"Failed to schedule specific update (errata ID: {errata_id_int}) for system {system_identifier} or unexpected API result format. Result: {api_result}")
697719
return ""
698720

699721
@write_tool()
@@ -705,7 +727,7 @@ async def add_system(
705727
ssh_user: str = "root",
706728
proxy_id: int = None,
707729
salt_ssh: bool = False,
708-
confirm: bool = False,
730+
confirm: Union[bool, str] = False,
709731
) -> str:
710732
"""
711733
Adds a new system to be managed by Uyuni.
@@ -721,10 +743,11 @@ async def add_system(
721743
ssh_user: The user to connect with via SSH (default: 'root').
722744
proxy_id: The system ID of a Uyuni proxy to use (optional).
723745
salt_ssh: Manage the system with Salt SSH (default: False).
724-
confirm: User confirmation is required to execute this action. Set to False
725-
by default. If False, the tool returns a confirmation message. The
726-
model must present this message to the user and, if they agree, call
727-
the tool again with this parameter set to True.
746+
confirm: User confirmation is required to execute this action. This parameter
747+
is `False` by default. To obtain the confirmation message that must
748+
be presented to the user, the model must first call the tool with
749+
`confirm=False`. If the user agrees, the model should call the tool
750+
a second time with `confirm=True`.
728751
729752
Returns:
730753
A confirmation message if 'confirm' is False.
@@ -735,6 +758,10 @@ async def add_system(
735758
log_string = f"Attempting to add system ID: {host}"
736759
logger.info(log_string)
737760
await ctx.info(log_string)
761+
762+
# Coerce string "false" to boolean False for models that send strings
763+
is_confirmed = str(confirm).lower() == 'true'
764+
738765
if ctx.session.check_client_capability(types.ClientCapabilities(elicitation=types.ElicitationCapability())):
739766
# Check for activation key
740767
if not activation_key:
@@ -761,7 +788,7 @@ async def add_system(
761788
await ctx.info(message)
762789
return message
763790

764-
if not confirm:
791+
if not is_confirmed:
765792
return f"CONFIRMATION REQUIRED: This will add system {host} with activation key {activation_key} to Uyuni. Do you confirm?"
766793

767794
ssh_priv_key_raw = os.environ.get('UYUNI_SSH_PRIV_KEY')
@@ -817,7 +844,7 @@ async def add_system(
817844

818845

819846
@write_tool()
820-
async def remove_system(system_identifier: Union[str, int], ctx: Context, cleanup: bool = True, confirm: bool = False) -> str:
847+
async def remove_system(system_identifier: Union[str, int], ctx: Context, cleanup: bool = True, confirm: Union[bool, str] = False) -> str:
821848
"""
822849
Removes/deletes a system from being managed by Uyuni.
823850
@@ -827,8 +854,11 @@ async def remove_system(system_identifier: Union[str, int], ctx: Context, cleanu
827854
system_identifier: The unique identifier of the system to remove. It can be the system name (e.g. "buildhost") or the system ID (e.g. 1000010000).
828855
cleanup: If True (default), Uyuni will attempt to run cleanup scripts on the client before deletion.
829856
If False, the system is deleted from Uyuni without attempting client-side cleanup.
830-
confirm: User confirmation is required. If False, the tool returns a confirmation prompt. The
831-
model must ask the user and call the tool again with confirm=True if they agree.
857+
confirm: User confirmation is required to execute this action. This parameter
858+
is `False` by default. To obtain the confirmation message that must
859+
be presented to the user, the model must first call the tool with
860+
`confirm=False`. If the user agrees, the model should call the tool
861+
a second time with `confirm=True`.
832862
833863
Returns:
834864
A confirmation message if 'confirm' is False.
@@ -837,6 +867,10 @@ async def remove_system(system_identifier: Union[str, int], ctx: Context, cleanu
837867
log_string = f"Attempting to remove system with id {system_identifier}"
838868
logger.info(log_string)
839869
await ctx.info(log_string)
870+
871+
# Coerce string "false" to boolean False for models that send strings
872+
is_confirmed = str(confirm).lower() == 'true'
873+
840874
system_id = await _resolve_system_id(system_identifier)
841875
if not system_id:
842876
return "" # Helper function already logged the reason for failure.
@@ -848,7 +882,7 @@ async def remove_system(system_identifier: Union[str, int], ctx: Context, cleanu
848882
logger.warning(message)
849883
return message
850884

851-
if not confirm:
885+
if not is_confirmed:
852886
return (f"CONFIRMATION REQUIRED: This will permanently remove system {system_id} from Uyuni. "
853887
f"Client-side cleanup is currently {'ENABLED' if cleanup else 'DISABLED'}. Do you confirm?")
854888

@@ -974,7 +1008,7 @@ async def get_systems_needing_security_update_for_cve(cve_identifier: str, ctx:
9741008
return list(affected_systems_map.values())
9751009

9761010
@mcp.tool()
977-
async def get_systems_needing_reboot(ctx: Context) -> List[Dict[str, Any]]:
1011+
async def get_systems_needing_reboot(ctx: Context) -> List[Dict[str, Any]]: # No change needed here
9781012
"""
9791013
Fetches a list of systems from the Uyuni server that require a reboot.
9801014
@@ -1024,14 +1058,18 @@ async def get_systems_needing_reboot(ctx: Context) -> List[Dict[str, Any]]:
10241058
return systems_needing_reboot_list
10251059

10261060
@write_tool()
1027-
async def schedule_system_reboot(system_identifier: Union[str, int], ctx:Context, confirm: bool = False) -> str:
1061+
async def schedule_system_reboot(system_identifier: Union[str, int], ctx:Context, confirm: Union[bool, str] = False) -> str:
10281062

10291063
"""
10301064
Schedules an immediate reboot for a specific system on the Uyuni server.
10311065
10321066
Args:
10331067
system_identifier: The unique identifier of the system. It can be the system name (e.g. "buildhost") or the system ID (e.g. 1000010000).
1034-
confirm: False by default. Only set confirm to True if the user has explicetely confirmed. Ask the user for confirmation.
1068+
confirm: User confirmation is required to execute this action. This parameter
1069+
is `False` by default. To obtain the confirmation message that must
1070+
be presented to the user, the model must first call the tool with
1071+
`confirm=False`. If the user agrees, the model should call the tool
1072+
a second time with `confirm=True`.
10351073
10361074
The reboot is scheduled to occur as soon as possible (effectively "now").
10371075
Returns:
@@ -1042,11 +1080,15 @@ async def schedule_system_reboot(system_identifier: Union[str, int], ctx:Context
10421080
log_string = f"Schedule system reboot for system {system_identifier}"
10431081
logger.info(log_string)
10441082
await ctx.info(log_string)
1083+
1084+
# Coerce string "false" to boolean False for models that send strings
1085+
is_confirmed = str(confirm).lower() == 'true'
1086+
10451087
system_id = await _resolve_system_id(system_identifier)
10461088
if not system_id:
10471089
return "" # Helper function already logged the reason for failure.
10481090

1049-
if not confirm:
1091+
if not is_confirmed:
10501092
return f"CONFIRMATION REQUIRED: This will reboot system {system_identifier}. Do you confirm?"
10511093

10521094
schedule_reboot_path = '/rhn/manager/api/system/scheduleReboot'
@@ -1126,7 +1168,7 @@ async def list_all_scheduled_actions(ctx: Context) -> List[Dict[str, Any]]:
11261168
return processed_actions_list
11271169

11281170
@write_tool()
1129-
async def cancel_action(action_id: int, ctx: Context, confirm: bool = False) -> str:
1171+
async def cancel_action(action_id: int, ctx: Context, confirm: Union[bool, str] = False) -> str:
11301172
"""
11311173
Cancels a specified action on the Uyuni server.
11321174
@@ -1135,7 +1177,11 @@ async def cancel_action(action_id: int, ctx: Context, confirm: bool = False) ->
11351177
11361178
Args:
11371179
action_id: The integer ID of the action to be canceled.
1138-
confirm: False by default. Only set confirm to True if the user has explicetely confirmed. Ask the user for confirmation.
1180+
confirm: User confirmation is required to execute this action. This parameter
1181+
is `False` by default. To obtain the confirmation message that must
1182+
be presented to the user, the model must first call the tool with
1183+
`confirm=False`. If the user agrees, the model should call the tool
1184+
a second time with `confirm=True`.
11391185
11401186
Returns:
11411187
str: A success message if the action was canceled,
@@ -1148,12 +1194,15 @@ async def cancel_action(action_id: int, ctx: Context, confirm: bool = False) ->
11481194
logger.info(log_string)
11491195
await ctx.info(log_string)
11501196

1197+
# Coerce string "false" to boolean False for models that send strings
1198+
is_confirmed = str(confirm).lower() == 'true'
1199+
11511200
cancel_actions_path = '/rhn/manager/api/schedule/cancelActions'
11521201

11531202
if not isinstance(action_id, int): # Basic type check, though FastMCP might handle this
11541203
return "Invalid action ID provided. Must be an integer."
11551204

1156-
if not confirm:
1205+
if not is_confirmed:
11571206
return f"CONFIRMATION REQUIRED: This will schedule action {action_id} to be canceled. Do you confirm?"
11581207

11591208
async with httpx.AsyncClient(verify=UYUNI_MCP_SSL_VERIFY) as client:

test/test_cases_ops.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
},
66
{
77
"id": "TC-OPS-001_confirm_request",
8-
"prompt": "Add a new system at host {new_system_host} with activation key '{key_deblike}'",
8+
"prompt": "Can you add a new system at host {new_system_host} with activation key '{key_deblike}'?",
99
"expected_output": "Return a message requiring confirmation."
1010
},
1111
{

test/test_cases_ops_2.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
},
66
{
77
"id": "TC-OPS-002_confirm_request",
8-
"prompt": "Remove system {new_system_host}",
8+
"prompt": "Can you remove system {new_system_host}?",
99
"expected_output": "Return a message requiring confirmation."
1010
},
1111
{

test/test_cases_sch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
},
77
{
88
"id": "TC-SCH-002_confirm_request",
9-
"prompt": "Cancel an action with a valid ID.",
9+
"prompt": "Can you cancel an action with a valid ID?",
1010
"expected_output": "Returns a message the requires confirmation"
1111
},
1212
{

0 commit comments

Comments
 (0)