awslabs.iam-mcp-server 1.0.1__py3-none-any.whl → 1.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,8 +22,17 @@ from awslabs.iam_mcp_server.errors import IamClientError, IamValidationError, ha
22
22
  from awslabs.iam_mcp_server.models import (
23
23
  AccessKey,
24
24
  AttachedPolicy,
25
+ CreateGroupResponse,
25
26
  CreateUserResponse,
27
+ GroupDetailsResponse,
28
+ GroupMembershipResponse,
29
+ GroupPolicyAttachmentResponse,
30
+ GroupsListResponse,
31
+ IamGroup,
26
32
  IamUser,
33
+ InlinePolicyListResponse,
34
+ InlinePolicyResponse,
35
+ ManagedPolicyResponse,
27
36
  UserDetailsResponse,
28
37
  UsersListResponse,
29
38
  )
@@ -45,10 +54,17 @@ mcp = FastMCP(
45
54
  1. **User Management**: Create, list, update, and delete IAM users
46
55
  2. **Role Management**: Create, list, update, and delete IAM roles
47
56
  3. **Policy Management**: Create, list, update, and delete IAM policies
48
- 4. **Group Management**: Create, list, update, and delete IAM groups
49
- 5. **Permission Management**: Attach/detach policies to users, roles, and groups
50
- 6. **Access Key Management**: Create, list, and delete access keys for users
51
- 7. **Security Analysis**: Analyze permissions, find unused resources, and security recommendations
57
+ 4. **Inline Policy Management**: Full CRUD operations for user and role inline policies
58
+ 5. **Group Management**: Create, list, update, and delete IAM groups
59
+ 6. **Permission Management**: Attach/detach policies to users, roles, and groups
60
+ 7. **Access Key Management**: Create, list, and delete access keys for users
61
+ 8. **Security Analysis**: Analyze permissions, find unused resources, and security recommendations
62
+
63
+ ## Inline Policy Management:
64
+ - **User Inline Policies**: Create, retrieve, update, delete, and list inline policies for users
65
+ - **Role Inline Policies**: Create, retrieve, update, delete, and list inline policies for roles
66
+ - **Policy Validation**: Automatic JSON validation for policy documents
67
+ - **Security Best Practices**: Built-in guidance for policy creation and management
52
68
 
53
69
  ## Security Best Practices:
54
70
  - Always follow the principle of least privilege
@@ -56,6 +72,8 @@ mcp = FastMCP(
56
72
  - Use roles instead of users for applications
57
73
  - Enable MFA where possible
58
74
  - Review and audit permissions regularly
75
+ - Prefer managed policies over inline policies for reusable permissions
76
+ - Test policies using simulate_principal_policy before applying
59
77
 
60
78
  ## Usage Requirements:
61
79
  - Requires valid AWS credentials with appropriate IAM permissions
@@ -180,7 +198,7 @@ async def get_user(
180
198
  inline_policies = inline_policies_response.get('PolicyNames', [])
181
199
 
182
200
  # Get groups
183
- groups_response = iam.get_groups_for_user(UserName=user_name)
201
+ groups_response = iam.list_groups_for_user(UserName=user_name)
184
202
  groups = [group['GroupName'] for group in groups_response.get('Groups', [])]
185
203
 
186
204
  # Get access keys
@@ -321,7 +339,7 @@ async def delete_user(
321
339
 
322
340
  if force:
323
341
  # Remove from all groups
324
- groups = iam.get_groups_for_user(UserName=user_name)
342
+ groups = iam.list_groups_for_user(UserName=user_name)
325
343
  for group in groups.get('Groups', []):
326
344
  iam.remove_user_from_group(GroupName=group['GroupName'], UserName=user_name)
327
345
 
@@ -542,6 +560,63 @@ async def list_policies(
542
560
  raise handle_iam_error(e)
543
561
 
544
562
 
563
+ @mcp.tool()
564
+ async def get_managed_policy_document(
565
+ policy_arn: str = Field(description='The ARN of the managed policy'),
566
+ version_id: Optional[str] = Field(
567
+ description='The version ID of the policy (defaults to current version)', default=None
568
+ ),
569
+ ) -> ManagedPolicyResponse:
570
+ """Retrieve the policy document for a managed policy.
571
+
572
+ This tool retrieves the policy document for a specific managed policy version.
573
+ Use this to examine the actual permissions and wildcards in managed policies.
574
+
575
+ Args:
576
+ policy_arn: The ARN of the managed policy
577
+ version_id: Optional version ID (defaults to current version)
578
+
579
+ Returns:
580
+ ManagedPolicyResponse containing the policy document and details
581
+ """
582
+ try:
583
+ logger.info(f'Getting managed policy document for: {policy_arn}')
584
+
585
+ if not policy_arn:
586
+ raise IamValidationError('Policy ARN is required')
587
+
588
+ iam = get_iam_client()
589
+
590
+ # Build parameters for the API call
591
+ kwargs = {'PolicyArn': policy_arn}
592
+ if version_id:
593
+ kwargs['VersionId'] = version_id
594
+
595
+ response = iam.get_policy_version(**kwargs)
596
+ policy_version = response['PolicyVersion']
597
+
598
+ # Extract policy name from ARN
599
+ policy_name = policy_arn.split('/')[-1]
600
+
601
+ result = ManagedPolicyResponse(
602
+ policy_arn=policy_arn,
603
+ policy_name=policy_name,
604
+ version_id=policy_version['VersionId'],
605
+ policy_document=json.dumps(policy_version['Document'], indent=2),
606
+ is_default_version=policy_version['IsDefaultVersion'],
607
+ create_date=policy_version['CreateDate'].isoformat(),
608
+ message=f'Successfully retrieved managed policy document for {policy_name}',
609
+ )
610
+
611
+ logger.info(f'Successfully retrieved managed policy document for: {policy_arn}')
612
+ return result
613
+
614
+ except Exception as e:
615
+ error = handle_iam_error(e)
616
+ logger.error(f'Error getting managed policy document: {error}')
617
+ raise error
618
+
619
+
545
620
  @mcp.tool()
546
621
  async def attach_user_policy(
547
622
  user_name: str = Field(description='The name of the IAM user'),
@@ -738,6 +813,761 @@ async def simulate_principal_policy(
738
813
  raise handle_iam_error(e)
739
814
 
740
815
 
816
+ # Group Management Tools
817
+
818
+
819
+ @mcp.tool()
820
+ async def list_groups(
821
+ path_prefix: Optional[str] = Field(
822
+ None, description='Path prefix to filter groups (e.g., "/division_abc/")'
823
+ ),
824
+ max_items: int = Field(100, description='Maximum number of groups to return'),
825
+ ) -> GroupsListResponse:
826
+ """List IAM groups in the account.
827
+
828
+ This tool retrieves a list of IAM groups from your AWS account with optional filtering.
829
+ Use this to get an overview of all groups or find specific groups by path prefix.
830
+
831
+ ## Usage Tips:
832
+ - Use path_prefix to filter groups by organizational structure
833
+ - Adjust max_items to control response size for large accounts
834
+ - Results may be paginated for accounts with many groups
835
+
836
+ Args:
837
+ path_prefix: Optional path prefix to filter groups
838
+ max_items: Maximum number of groups to return
839
+
840
+ Returns:
841
+ GroupsListResponse containing list of groups and metadata
842
+ """
843
+ if Context.is_readonly():
844
+ # List operations are allowed in read-only mode
845
+ pass
846
+
847
+ try:
848
+ iam = get_iam_client()
849
+
850
+ kwargs: Dict[str, Union[int, str]] = {'MaxItems': max_items}
851
+ if path_prefix:
852
+ kwargs['PathPrefix'] = path_prefix
853
+
854
+ response = iam.list_groups(**kwargs)
855
+
856
+ groups = []
857
+ for group_data in response.get('Groups', []):
858
+ group = IamGroup(
859
+ group_name=group_data['GroupName'],
860
+ group_id=group_data['GroupId'],
861
+ arn=group_data['Arn'],
862
+ path=group_data['Path'],
863
+ create_date=group_data['CreateDate'].isoformat(),
864
+ )
865
+ groups.append(group)
866
+
867
+ return GroupsListResponse(
868
+ groups=groups,
869
+ is_truncated=response.get('IsTruncated', False),
870
+ marker=response.get('Marker'),
871
+ count=len(groups),
872
+ )
873
+
874
+ except Exception as e:
875
+ raise handle_iam_error(e)
876
+
877
+
878
+ @mcp.tool()
879
+ async def get_group(
880
+ group_name: str = Field(description='The name of the IAM group to retrieve'),
881
+ ) -> GroupDetailsResponse:
882
+ """Get detailed information about a specific IAM group.
883
+
884
+ This tool retrieves comprehensive information about an IAM group including
885
+ group members, attached policies, and inline policies. Use this to get
886
+ a complete picture of a group's configuration and membership.
887
+
888
+ ## Usage Tips:
889
+ - Use this after list_groups to get detailed information about specific groups
890
+ - Review attached policies to understand group permissions
891
+ - Check group members to see who has these permissions
892
+
893
+ Args:
894
+ group_name: The name of the IAM group
895
+
896
+ Returns:
897
+ GroupDetailsResponse containing comprehensive group information
898
+ """
899
+ if Context.is_readonly():
900
+ # Get operations are allowed in read-only mode
901
+ pass
902
+
903
+ try:
904
+ iam = get_iam_client()
905
+
906
+ # Get group details and members
907
+ group_response = iam.get_group(GroupName=group_name)
908
+ group_data = group_response['Group']
909
+
910
+ group = IamGroup(
911
+ group_name=group_data['GroupName'],
912
+ group_id=group_data['GroupId'],
913
+ arn=group_data['Arn'],
914
+ path=group_data['Path'],
915
+ create_date=group_data['CreateDate'].isoformat(),
916
+ )
917
+
918
+ # Get group members
919
+ users = [user['UserName'] for user in group_response.get('Users', [])]
920
+
921
+ # Get attached managed policies
922
+ attached_policies_response = iam.list_attached_group_policies(GroupName=group_name)
923
+ attached_policies = [
924
+ AttachedPolicy(policy_name=policy['PolicyName'], policy_arn=policy['PolicyArn'])
925
+ for policy in attached_policies_response.get('AttachedPolicies', [])
926
+ ]
927
+
928
+ # Get inline policies
929
+ inline_policies_response = iam.list_group_policies(GroupName=group_name)
930
+ inline_policies = inline_policies_response.get('PolicyNames', [])
931
+
932
+ return GroupDetailsResponse(
933
+ group=group,
934
+ users=users,
935
+ attached_policies=attached_policies,
936
+ inline_policies=inline_policies,
937
+ )
938
+
939
+ except Exception as e:
940
+ raise handle_iam_error(e)
941
+
942
+
943
+ @mcp.tool()
944
+ async def create_group(
945
+ group_name: str = Field(description='The name of the new IAM group'),
946
+ path: str = Field('/', description='The path for the group'),
947
+ ) -> CreateGroupResponse:
948
+ """Create a new IAM group.
949
+
950
+ This tool creates a new IAM group in your AWS account. The group will be created
951
+ without any permissions by default - you'll need to attach policies separately.
952
+
953
+ ## Security Best Practices:
954
+ - Use descriptive group names that indicate the group's purpose
955
+ - Set appropriate paths for organizational structure
956
+ - Follow the principle of least privilege when assigning permissions later
957
+
958
+ Args:
959
+ group_name: The name of the new IAM group
960
+ path: The path for the group (default: '/')
961
+
962
+ Returns:
963
+ CreateGroupResponse containing the created group details
964
+ """
965
+ if Context.is_readonly():
966
+ raise IamValidationError('Cannot create group in read-only mode')
967
+
968
+ try:
969
+ iam = get_iam_client()
970
+
971
+ response = iam.create_group(GroupName=group_name, Path=path)
972
+
973
+ group_data = response['Group']
974
+ group = IamGroup(
975
+ group_name=group_data['GroupName'],
976
+ group_id=group_data['GroupId'],
977
+ arn=group_data['Arn'],
978
+ path=group_data['Path'],
979
+ create_date=group_data['CreateDate'].isoformat(),
980
+ )
981
+
982
+ return CreateGroupResponse(
983
+ group=group, message=f'Successfully created IAM group: {group_name}'
984
+ )
985
+
986
+ except Exception as e:
987
+ raise handle_iam_error(e)
988
+
989
+
990
+ @mcp.tool()
991
+ async def delete_group(
992
+ group_name: str = Field(description='The name of the IAM group to delete'),
993
+ force: bool = Field(
994
+ False, description='Force delete by removing all members and policies first'
995
+ ),
996
+ ) -> Dict[str, str]:
997
+ """Delete an IAM group.
998
+
999
+ Args:
1000
+ group_name: The name of the IAM group to delete
1001
+ force: If True, removes all members and attached policies first
1002
+
1003
+ Returns:
1004
+ Dictionary containing deletion status
1005
+ """
1006
+ if Context.is_readonly():
1007
+ raise IamValidationError('Cannot delete group in read-only mode')
1008
+
1009
+ try:
1010
+ iam = get_iam_client()
1011
+
1012
+ if force:
1013
+ # Remove all users from the group
1014
+ group_response = iam.get_group(GroupName=group_name)
1015
+ for user in group_response.get('Users', []):
1016
+ iam.remove_user_from_group(GroupName=group_name, UserName=user['UserName'])
1017
+
1018
+ # Detach all managed policies
1019
+ attached_policies = iam.list_attached_group_policies(GroupName=group_name)
1020
+ for policy in attached_policies.get('AttachedPolicies', []):
1021
+ iam.detach_group_policy(GroupName=group_name, PolicyArn=policy['PolicyArn'])
1022
+
1023
+ # Delete all inline policies
1024
+ inline_policies = iam.list_group_policies(GroupName=group_name)
1025
+ for policy_name in inline_policies.get('PolicyNames', []):
1026
+ iam.delete_group_policy(GroupName=group_name, PolicyName=policy_name)
1027
+
1028
+ # Delete the group
1029
+ iam.delete_group(GroupName=group_name)
1030
+
1031
+ return {'message': f'Successfully deleted IAM group: {group_name}'}
1032
+
1033
+ except Exception as e:
1034
+ raise handle_iam_error(e)
1035
+
1036
+
1037
+ @mcp.tool()
1038
+ async def add_user_to_group(
1039
+ group_name: str = Field(description='The name of the IAM group'),
1040
+ user_name: str = Field(description='The name of the IAM user'),
1041
+ ) -> GroupMembershipResponse:
1042
+ """Add a user to an IAM group.
1043
+
1044
+ Args:
1045
+ group_name: The name of the IAM group
1046
+ user_name: The name of the IAM user
1047
+
1048
+ Returns:
1049
+ GroupMembershipResponse containing operation status
1050
+ """
1051
+ if Context.is_readonly():
1052
+ raise IamValidationError('Cannot add user to group in read-only mode')
1053
+
1054
+ try:
1055
+ iam = get_iam_client()
1056
+ iam.add_user_to_group(GroupName=group_name, UserName=user_name)
1057
+
1058
+ return GroupMembershipResponse(
1059
+ message=f'Successfully added user {user_name} to group {group_name}',
1060
+ group_name=group_name,
1061
+ user_name=user_name,
1062
+ )
1063
+
1064
+ except Exception as e:
1065
+ raise handle_iam_error(e)
1066
+
1067
+
1068
+ @mcp.tool()
1069
+ async def remove_user_from_group(
1070
+ group_name: str = Field(description='The name of the IAM group'),
1071
+ user_name: str = Field(description='The name of the IAM user'),
1072
+ ) -> GroupMembershipResponse:
1073
+ """Remove a user from an IAM group.
1074
+
1075
+ Args:
1076
+ group_name: The name of the IAM group
1077
+ user_name: The name of the IAM user
1078
+
1079
+ Returns:
1080
+ GroupMembershipResponse containing operation status
1081
+ """
1082
+ if Context.is_readonly():
1083
+ raise IamValidationError('Cannot remove user from group in read-only mode')
1084
+
1085
+ try:
1086
+ iam = get_iam_client()
1087
+ iam.remove_user_from_group(GroupName=group_name, UserName=user_name)
1088
+
1089
+ return GroupMembershipResponse(
1090
+ message=f'Successfully removed user {user_name} from group {group_name}',
1091
+ group_name=group_name,
1092
+ user_name=user_name,
1093
+ )
1094
+
1095
+ except Exception as e:
1096
+ raise handle_iam_error(e)
1097
+
1098
+
1099
+ @mcp.tool()
1100
+ async def attach_group_policy(
1101
+ group_name: str = Field(description='The name of the IAM group'),
1102
+ policy_arn: str = Field(description='The ARN of the policy to attach'),
1103
+ ) -> GroupPolicyAttachmentResponse:
1104
+ """Attach a managed policy to an IAM group.
1105
+
1106
+ Args:
1107
+ group_name: The name of the IAM group
1108
+ policy_arn: The ARN of the policy to attach
1109
+
1110
+ Returns:
1111
+ GroupPolicyAttachmentResponse containing operation status
1112
+ """
1113
+ if Context.is_readonly():
1114
+ raise IamValidationError('Cannot attach policy to group in read-only mode')
1115
+
1116
+ try:
1117
+ iam = get_iam_client()
1118
+ iam.attach_group_policy(GroupName=group_name, PolicyArn=policy_arn)
1119
+
1120
+ return GroupPolicyAttachmentResponse(
1121
+ message=f'Successfully attached policy {policy_arn} to group {group_name}',
1122
+ group_name=group_name,
1123
+ policy_arn=policy_arn,
1124
+ )
1125
+
1126
+ except Exception as e:
1127
+ raise handle_iam_error(e)
1128
+
1129
+
1130
+ @mcp.tool()
1131
+ async def detach_group_policy(
1132
+ group_name: str = Field(description='The name of the IAM group'),
1133
+ policy_arn: str = Field(description='The ARN of the policy to detach'),
1134
+ ) -> GroupPolicyAttachmentResponse:
1135
+ """Detach a managed policy from an IAM group.
1136
+
1137
+ Args:
1138
+ group_name: The name of the IAM group
1139
+ policy_arn: The ARN of the policy to detach
1140
+
1141
+ Returns:
1142
+ GroupPolicyAttachmentResponse containing operation status
1143
+ """
1144
+ if Context.is_readonly():
1145
+ raise IamValidationError('Cannot detach policy from group in read-only mode')
1146
+
1147
+ try:
1148
+ iam = get_iam_client()
1149
+ iam.detach_group_policy(GroupName=group_name, PolicyArn=policy_arn)
1150
+
1151
+ return GroupPolicyAttachmentResponse(
1152
+ message=f'Successfully detached policy {policy_arn} from group {group_name}',
1153
+ group_name=group_name,
1154
+ policy_arn=policy_arn,
1155
+ )
1156
+
1157
+ except Exception as e:
1158
+ raise handle_iam_error(e)
1159
+
1160
+
1161
+ # Inline Policy Management Tools
1162
+
1163
+
1164
+ @mcp.tool()
1165
+ async def put_user_policy(
1166
+ user_name: str = Field(description='The name of the IAM user'),
1167
+ policy_name: str = Field(description='The name of the inline policy'),
1168
+ policy_document: Union[str, dict] = Field(
1169
+ description='The policy document in JSON format (string or dict)'
1170
+ ),
1171
+ ) -> InlinePolicyResponse:
1172
+ """Create or update an inline policy for an IAM user.
1173
+
1174
+ This tool creates a new inline policy or updates an existing one for the specified user.
1175
+ Inline policies are directly embedded in a single user, role, or group and have a one-to-one
1176
+ relationship with the identity.
1177
+
1178
+ ## Security Best Practices:
1179
+ - Follow the principle of least privilege when creating policies
1180
+ - Use managed policies for common permissions that can be reused
1181
+ - Regularly review and audit inline policies
1182
+ - Test policies using simulate_principal_policy before applying
1183
+
1184
+ Args:
1185
+ user_name: The name of the IAM user
1186
+ policy_name: The name of the inline policy
1187
+ policy_document: The policy document in JSON format
1188
+
1189
+ Returns:
1190
+ InlinePolicyResponse containing the policy details and operation status
1191
+ """
1192
+ try:
1193
+ logger.info(f'Creating/updating inline policy {policy_name} for user: {user_name}')
1194
+
1195
+ # Check if server is in read-only mode
1196
+ if Context.is_readonly():
1197
+ raise IamClientError(
1198
+ 'Cannot create/update inline policy: server is running in read-only mode'
1199
+ )
1200
+
1201
+ if not user_name or not policy_name:
1202
+ raise IamValidationError('User name and policy name are required')
1203
+
1204
+ iam = get_iam_client()
1205
+
1206
+ # Handle both string and dict types
1207
+ if isinstance(policy_document, dict):
1208
+ policy_doc = json.dumps(policy_document)
1209
+ else:
1210
+ policy_doc = policy_document
1211
+ # Validate JSON
1212
+ try:
1213
+ json.loads(policy_doc)
1214
+ except json.JSONDecodeError:
1215
+ raise IamValidationError('Invalid JSON in policy_document')
1216
+
1217
+ iam.put_user_policy(UserName=user_name, PolicyName=policy_name, PolicyDocument=policy_doc)
1218
+
1219
+ result = InlinePolicyResponse(
1220
+ policy_name=policy_name,
1221
+ policy_document=policy_doc,
1222
+ user_name=user_name,
1223
+ role_name=None,
1224
+ message=f'Successfully created/updated inline policy {policy_name} for user {user_name}',
1225
+ )
1226
+
1227
+ logger.info(
1228
+ f'Successfully created/updated inline policy {policy_name} for user: {user_name}'
1229
+ )
1230
+ return result
1231
+
1232
+ except Exception as e:
1233
+ error = handle_iam_error(e)
1234
+ logger.error(f'Error creating/updating inline policy: {error}')
1235
+ raise error
1236
+
1237
+
1238
+ @mcp.tool()
1239
+ async def get_user_policy(
1240
+ user_name: str = Field(description='The name of the IAM user'),
1241
+ policy_name: str = Field(description='The name of the inline policy'),
1242
+ ) -> InlinePolicyResponse:
1243
+ """Retrieve an inline policy for an IAM user.
1244
+
1245
+ This tool retrieves the policy document for a specific inline policy attached to a user.
1246
+
1247
+ Args:
1248
+ user_name: The name of the IAM user
1249
+ policy_name: The name of the inline policy
1250
+
1251
+ Returns:
1252
+ InlinePolicyResponse containing the policy document and details
1253
+ """
1254
+ try:
1255
+ logger.info(f'Getting inline policy {policy_name} for user: {user_name}')
1256
+
1257
+ if not user_name or not policy_name:
1258
+ raise IamValidationError('User name and policy name are required')
1259
+
1260
+ iam = get_iam_client()
1261
+
1262
+ response = iam.get_user_policy(UserName=user_name, PolicyName=policy_name)
1263
+
1264
+ result = InlinePolicyResponse(
1265
+ policy_name=response['PolicyName'],
1266
+ policy_document=response['PolicyDocument'],
1267
+ user_name=response['UserName'],
1268
+ role_name=None,
1269
+ message=f'Successfully retrieved inline policy {policy_name} for user {user_name}',
1270
+ )
1271
+
1272
+ logger.info(f'Successfully retrieved inline policy {policy_name} for user: {user_name}')
1273
+ return result
1274
+
1275
+ except Exception as e:
1276
+ error = handle_iam_error(e)
1277
+ logger.error(f'Error getting inline policy: {error}')
1278
+ raise error
1279
+
1280
+
1281
+ @mcp.tool()
1282
+ async def delete_user_policy(
1283
+ user_name: str = Field(description='The name of the IAM user'),
1284
+ policy_name: str = Field(description='The name of the inline policy to delete'),
1285
+ ) -> Dict[str, Any]:
1286
+ """Delete an inline policy from an IAM user.
1287
+
1288
+ This tool removes an inline policy from the specified user. The policy document
1289
+ will be permanently deleted and cannot be recovered.
1290
+
1291
+ Args:
1292
+ user_name: The name of the IAM user
1293
+ policy_name: The name of the inline policy to delete
1294
+
1295
+ Returns:
1296
+ Dictionary containing deletion status
1297
+ """
1298
+ try:
1299
+ logger.info(f'Deleting inline policy {policy_name} from user: {user_name}')
1300
+
1301
+ # Check if server is in read-only mode
1302
+ if Context.is_readonly():
1303
+ raise IamClientError(
1304
+ 'Cannot delete inline policy: server is running in read-only mode'
1305
+ )
1306
+
1307
+ if not user_name or not policy_name:
1308
+ raise IamValidationError('User name and policy name are required')
1309
+
1310
+ iam = get_iam_client()
1311
+
1312
+ iam.delete_user_policy(UserName=user_name, PolicyName=policy_name)
1313
+
1314
+ result = {
1315
+ 'message': f'Successfully deleted inline policy {policy_name} from user {user_name}',
1316
+ 'user_name': user_name,
1317
+ 'policy_name': policy_name,
1318
+ }
1319
+
1320
+ logger.info(f'Successfully deleted inline policy {policy_name} from user: {user_name}')
1321
+ return result
1322
+
1323
+ except Exception as e:
1324
+ error = handle_iam_error(e)
1325
+ logger.error(f'Error deleting inline policy: {error}')
1326
+ raise error
1327
+
1328
+
1329
+ # Role Inline Policy Management Tools
1330
+
1331
+
1332
+ @mcp.tool()
1333
+ async def put_role_policy(
1334
+ role_name: str = Field(description='The name of the IAM role'),
1335
+ policy_name: str = Field(description='The name of the inline policy'),
1336
+ policy_document: Union[str, dict] = Field(
1337
+ description='The policy document in JSON format (string or dict)'
1338
+ ),
1339
+ ) -> InlinePolicyResponse:
1340
+ """Create or update an inline policy for an IAM role.
1341
+
1342
+ This tool creates a new inline policy or updates an existing one for the specified role.
1343
+ Inline policies are directly embedded in a single user, role, or group and have a one-to-one
1344
+ relationship with the identity.
1345
+
1346
+ Args:
1347
+ role_name: The name of the IAM role
1348
+ policy_name: The name of the inline policy
1349
+ policy_document: The policy document in JSON format
1350
+
1351
+ Returns:
1352
+ InlinePolicyResponse containing the policy details and operation status
1353
+ """
1354
+ try:
1355
+ logger.info(f'Creating/updating inline policy {policy_name} for role: {role_name}')
1356
+
1357
+ # Check if server is in read-only mode
1358
+ if Context.is_readonly():
1359
+ raise IamClientError(
1360
+ 'Cannot create/update inline policy: server is running in read-only mode'
1361
+ )
1362
+
1363
+ if not role_name or not policy_name:
1364
+ raise IamValidationError('Role name and policy name are required')
1365
+
1366
+ iam = get_iam_client()
1367
+
1368
+ # Handle both string and dict types
1369
+ if isinstance(policy_document, dict):
1370
+ policy_doc = json.dumps(policy_document)
1371
+ else:
1372
+ policy_doc = policy_document
1373
+ # Validate JSON
1374
+ try:
1375
+ json.loads(policy_doc)
1376
+ except json.JSONDecodeError:
1377
+ raise IamValidationError('Invalid JSON in policy_document')
1378
+
1379
+ iam.put_role_policy(RoleName=role_name, PolicyName=policy_name, PolicyDocument=policy_doc)
1380
+
1381
+ result = InlinePolicyResponse(
1382
+ policy_name=policy_name,
1383
+ policy_document=policy_doc,
1384
+ user_name=None,
1385
+ role_name=role_name,
1386
+ message=f'Successfully created/updated inline policy {policy_name} for role {role_name}',
1387
+ )
1388
+
1389
+ logger.info(
1390
+ f'Successfully created/updated inline policy {policy_name} for role: {role_name}'
1391
+ )
1392
+ return result
1393
+
1394
+ except Exception as e:
1395
+ error = handle_iam_error(e)
1396
+ logger.error(f'Error creating/updating inline policy: {error}')
1397
+ raise error
1398
+
1399
+
1400
+ @mcp.tool()
1401
+ async def get_role_policy(
1402
+ role_name: str = Field(description='The name of the IAM role'),
1403
+ policy_name: str = Field(description='The name of the inline policy'),
1404
+ ) -> InlinePolicyResponse:
1405
+ """Retrieve an inline policy for an IAM role.
1406
+
1407
+ This tool retrieves the policy document for a specific inline policy attached to a role.
1408
+
1409
+ Args:
1410
+ role_name: The name of the IAM role
1411
+ policy_name: The name of the inline policy
1412
+
1413
+ Returns:
1414
+ InlinePolicyResponse containing the policy document and details
1415
+ """
1416
+ try:
1417
+ logger.info(f'Getting inline policy {policy_name} for role: {role_name}')
1418
+
1419
+ if not role_name or not policy_name:
1420
+ raise IamValidationError('Role name and policy name are required')
1421
+
1422
+ iam = get_iam_client()
1423
+
1424
+ response = iam.get_role_policy(RoleName=role_name, PolicyName=policy_name)
1425
+
1426
+ result = InlinePolicyResponse(
1427
+ policy_name=response['PolicyName'],
1428
+ policy_document=response['PolicyDocument'],
1429
+ user_name=None,
1430
+ role_name=response['RoleName'],
1431
+ message=f'Successfully retrieved inline policy {policy_name} for role {role_name}',
1432
+ )
1433
+
1434
+ logger.info(f'Successfully retrieved inline policy {policy_name} for role: {role_name}')
1435
+ return result
1436
+
1437
+ except Exception as e:
1438
+ error = handle_iam_error(e)
1439
+ logger.error(f'Error getting inline policy: {error}')
1440
+ raise error
1441
+
1442
+
1443
+ @mcp.tool()
1444
+ async def delete_role_policy(
1445
+ role_name: str = Field(description='The name of the IAM role'),
1446
+ policy_name: str = Field(description='The name of the inline policy to delete'),
1447
+ ) -> Dict[str, Any]:
1448
+ """Delete an inline policy from an IAM role.
1449
+
1450
+ This tool removes an inline policy from the specified role. The policy document
1451
+ will be permanently deleted and cannot be recovered.
1452
+
1453
+ Args:
1454
+ role_name: The name of the IAM role
1455
+ policy_name: The name of the inline policy to delete
1456
+
1457
+ Returns:
1458
+ Dictionary containing deletion status
1459
+ """
1460
+ try:
1461
+ logger.info(f'Deleting inline policy {policy_name} from role: {role_name}')
1462
+
1463
+ # Check if server is in read-only mode
1464
+ if Context.is_readonly():
1465
+ raise IamClientError(
1466
+ 'Cannot delete inline policy: server is running in read-only mode'
1467
+ )
1468
+
1469
+ if not role_name or not policy_name:
1470
+ raise IamValidationError('Role name and policy name are required')
1471
+
1472
+ iam = get_iam_client()
1473
+
1474
+ iam.delete_role_policy(RoleName=role_name, PolicyName=policy_name)
1475
+
1476
+ result = {
1477
+ 'message': f'Successfully deleted inline policy {policy_name} from role {role_name}',
1478
+ 'role_name': role_name,
1479
+ 'policy_name': policy_name,
1480
+ }
1481
+
1482
+ logger.info(f'Successfully deleted inline policy {policy_name} from role: {role_name}')
1483
+ return result
1484
+
1485
+ except Exception as e:
1486
+ error = handle_iam_error(e)
1487
+ logger.error(f'Error deleting inline policy: {error}')
1488
+ raise error
1489
+
1490
+
1491
+ @mcp.tool()
1492
+ async def list_user_policies(
1493
+ user_name: str = Field(description='The name of the IAM user'),
1494
+ ) -> InlinePolicyListResponse:
1495
+ """List all inline policies for an IAM user.
1496
+
1497
+ This tool retrieves the names of all inline policies attached to the specified user.
1498
+
1499
+ Args:
1500
+ user_name: The name of the IAM user
1501
+
1502
+ Returns:
1503
+ InlinePolicyListResponse containing the list of policy names
1504
+ """
1505
+ try:
1506
+ logger.info(f'Listing inline policies for user: {user_name}')
1507
+
1508
+ if not user_name:
1509
+ raise IamValidationError('User name is required')
1510
+
1511
+ iam = get_iam_client()
1512
+
1513
+ response = iam.list_user_policies(UserName=user_name)
1514
+
1515
+ result = InlinePolicyListResponse(
1516
+ policy_names=response.get('PolicyNames', []),
1517
+ user_name=user_name,
1518
+ role_name=None,
1519
+ count=len(response.get('PolicyNames', [])),
1520
+ )
1521
+
1522
+ logger.info(f'Successfully listed {result.count} inline policies for user: {user_name}')
1523
+ return result
1524
+
1525
+ except Exception as e:
1526
+ error = handle_iam_error(e)
1527
+ logger.error(f'Error listing inline policies: {error}')
1528
+ raise error
1529
+
1530
+
1531
+ @mcp.tool()
1532
+ async def list_role_policies(
1533
+ role_name: str = Field(description='The name of the IAM role'),
1534
+ ) -> InlinePolicyListResponse:
1535
+ """List all inline policies for an IAM role.
1536
+
1537
+ This tool retrieves the names of all inline policies attached to the specified role.
1538
+
1539
+ Args:
1540
+ role_name: The name of the IAM role
1541
+
1542
+ Returns:
1543
+ InlinePolicyListResponse containing the list of policy names
1544
+ """
1545
+ try:
1546
+ logger.info(f'Listing inline policies for role: {role_name}')
1547
+
1548
+ if not role_name:
1549
+ raise IamValidationError('Role name is required')
1550
+
1551
+ iam = get_iam_client()
1552
+
1553
+ response = iam.list_role_policies(RoleName=role_name)
1554
+
1555
+ result = InlinePolicyListResponse(
1556
+ policy_names=response.get('PolicyNames', []),
1557
+ user_name=None,
1558
+ role_name=role_name,
1559
+ count=len(response.get('PolicyNames', [])),
1560
+ )
1561
+
1562
+ logger.info(f'Successfully listed {result.count} inline policies for role: {role_name}')
1563
+ return result
1564
+
1565
+ except Exception as e:
1566
+ error = handle_iam_error(e)
1567
+ logger.error(f'Error listing inline policies: {error}')
1568
+ raise error
1569
+
1570
+
741
1571
  def main():
742
1572
  """Run the MCP server with CLI argument support."""
743
1573
  parser = argparse.ArgumentParser(
@@ -745,23 +1575,20 @@ def main():
745
1575
  )
746
1576
  parser.add_argument(
747
1577
  '--readonly',
748
- action=argparse.BooleanOptionalAction,
749
- help='Prevents the MCP server from performing mutating operations',
750
- default=False,
1578
+ action='store_true',
1579
+ help='Run server in read-only mode (prevents all mutating operations)',
751
1580
  )
752
- parser.add_argument('--region', help='AWS region to use for operations')
753
1581
 
754
1582
  args = parser.parse_args()
755
1583
 
756
- # Initialize context with configuration
757
- Context.initialize(readonly=args.readonly, region=args.region)
758
-
759
- if args.region:
760
- logger.info(f'Using AWS region: {args.region}')
761
-
1584
+ # Set read-only mode if specified
762
1585
  if args.readonly:
763
- logger.info('Running in read-only mode - mutating operations will be disabled')
1586
+ Context.set_readonly(True)
1587
+ logger.info('Server started in READ-ONLY mode - all mutating operations are disabled')
1588
+ else:
1589
+ logger.info('Server started in FULL ACCESS mode')
764
1590
 
1591
+ # Run the MCP server
765
1592
  mcp.run()
766
1593
 
767
1594