awslabs.iam-mcp-server 1.0.1__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.
@@ -0,0 +1,769 @@
1
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """AWS IAM MCP Server implementation."""
16
+
17
+ import argparse
18
+ import json
19
+ from awslabs.iam_mcp_server.aws_client import get_iam_client
20
+ from awslabs.iam_mcp_server.context import Context
21
+ from awslabs.iam_mcp_server.errors import IamClientError, IamValidationError, handle_iam_error
22
+ from awslabs.iam_mcp_server.models import (
23
+ AccessKey,
24
+ AttachedPolicy,
25
+ CreateUserResponse,
26
+ IamUser,
27
+ UserDetailsResponse,
28
+ UsersListResponse,
29
+ )
30
+ from loguru import logger
31
+ from mcp.server.fastmcp import FastMCP
32
+ from mcp.types import CallToolResult
33
+ from pydantic import Field
34
+ from typing import Any, Dict, List, Optional, Union
35
+
36
+
37
+ mcp = FastMCP(
38
+ 'awslabs.iam-mcp-server',
39
+ instructions="""
40
+ # AWS IAM MCP Server
41
+
42
+ This MCP server provides comprehensive AWS Identity and Access Management (IAM) capabilities:
43
+
44
+ ## Core Features:
45
+ 1. **User Management**: Create, list, update, and delete IAM users
46
+ 2. **Role Management**: Create, list, update, and delete IAM roles
47
+ 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
52
+
53
+ ## Security Best Practices:
54
+ - Always follow the principle of least privilege
55
+ - Regularly rotate access keys
56
+ - Use roles instead of users for applications
57
+ - Enable MFA where possible
58
+ - Review and audit permissions regularly
59
+
60
+ ## Usage Requirements:
61
+ - Requires valid AWS credentials with appropriate IAM permissions
62
+ - Some operations may be restricted in read-only mode
63
+ - Always test policy changes in a safe environment first
64
+ """,
65
+ dependencies=['pydantic', 'loguru', 'boto3', 'botocore'],
66
+ )
67
+
68
+
69
+ @mcp.tool()
70
+ async def list_users(
71
+ ctx: CallToolResult,
72
+ path_prefix: Optional[str] = Field(
73
+ description='Path prefix to filter users (e.g., "/division_abc/")', default=None
74
+ ),
75
+ max_items: int = Field(description='Maximum number of users to return', default=100),
76
+ ) -> UsersListResponse:
77
+ """List IAM users in the account.
78
+
79
+ This tool retrieves a list of IAM users from your AWS account with optional filtering.
80
+ Use this to get an overview of all users or find specific users by path prefix.
81
+
82
+ ## Usage Tips:
83
+ - Use path_prefix to filter users by organizational structure
84
+ - Adjust max_items to control response size for large accounts
85
+ - Results may be paginated for accounts with many users
86
+
87
+ Args:
88
+ ctx: MCP context for error reporting
89
+ path_prefix: Optional path prefix to filter users
90
+ max_items: Maximum number of users to return
91
+
92
+ Returns:
93
+ UsersListResponse containing list of users and metadata
94
+ """
95
+ try:
96
+ logger.info(f"Listing IAM users with path_prefix='{path_prefix}', max_items={max_items}")
97
+
98
+ iam = get_iam_client()
99
+
100
+ kwargs: Dict[str, Any] = {'MaxItems': max_items}
101
+ if path_prefix:
102
+ kwargs['PathPrefix'] = path_prefix
103
+
104
+ response = iam.list_users(**kwargs)
105
+
106
+ users = []
107
+ for user in response.get('Users', []):
108
+ users.append(
109
+ IamUser(
110
+ user_name=user['UserName'],
111
+ user_id=user['UserId'],
112
+ arn=user['Arn'],
113
+ path=user['Path'],
114
+ create_date=user['CreateDate'].isoformat(),
115
+ password_last_used=user.get('PasswordLastUsed', '').isoformat()
116
+ if user.get('PasswordLastUsed')
117
+ else None,
118
+ )
119
+ )
120
+
121
+ result = UsersListResponse(
122
+ users=users,
123
+ is_truncated=response.get('IsTruncated', False),
124
+ marker=response.get('Marker'),
125
+ count=len(users),
126
+ )
127
+
128
+ logger.info(f'Successfully listed {len(users)} IAM users')
129
+ return result
130
+
131
+ except Exception as e:
132
+ error = handle_iam_error(e)
133
+ logger.error(f'Error listing users: {error}')
134
+ raise error
135
+
136
+
137
+ @mcp.tool()
138
+ async def get_user(
139
+ ctx: CallToolResult, user_name: str = Field(description='The name of the IAM user to retrieve')
140
+ ) -> UserDetailsResponse:
141
+ """Get detailed information about a specific IAM user.
142
+
143
+ This tool retrieves comprehensive information about an IAM user including
144
+ attached policies, group memberships, and access keys. Use this to get
145
+ a complete picture of a user's permissions and configuration.
146
+
147
+ ## Usage Tips:
148
+ - Use this after list_users to get detailed information about specific users
149
+ - Review attached policies to understand user permissions
150
+ - Check access keys to identify potential security issues
151
+
152
+ Args:
153
+ ctx: MCP context for error reporting
154
+ user_name: The name of the IAM user
155
+
156
+ Returns:
157
+ UserDetailsResponse containing comprehensive user information
158
+ """
159
+ try:
160
+ logger.info(f'Getting details for IAM user: {user_name}')
161
+
162
+ if not user_name:
163
+ raise IamValidationError('User name is required')
164
+
165
+ iam = get_iam_client()
166
+
167
+ # Get user details
168
+ user_response = iam.get_user(UserName=user_name)
169
+ user = user_response['User']
170
+
171
+ # Get attached policies
172
+ attached_policies_response = iam.list_attached_user_policies(UserName=user_name)
173
+ attached_policies = [
174
+ AttachedPolicy(policy_name=policy['PolicyName'], policy_arn=policy['PolicyArn'])
175
+ for policy in attached_policies_response.get('AttachedPolicies', [])
176
+ ]
177
+
178
+ # Get inline policies
179
+ inline_policies_response = iam.list_user_policies(UserName=user_name)
180
+ inline_policies = inline_policies_response.get('PolicyNames', [])
181
+
182
+ # Get groups
183
+ groups_response = iam.get_groups_for_user(UserName=user_name)
184
+ groups = [group['GroupName'] for group in groups_response.get('Groups', [])]
185
+
186
+ # Get access keys
187
+ access_keys_response = iam.list_access_keys(UserName=user_name)
188
+ access_keys = [
189
+ AccessKey(
190
+ access_key_id=key['AccessKeyId'],
191
+ status=key['Status'],
192
+ create_date=key['CreateDate'].isoformat(),
193
+ )
194
+ for key in access_keys_response.get('AccessKeyMetadata', [])
195
+ ]
196
+
197
+ user_details = IamUser(
198
+ user_name=user['UserName'],
199
+ user_id=user['UserId'],
200
+ arn=user['Arn'],
201
+ path=user['Path'],
202
+ create_date=user['CreateDate'].isoformat(),
203
+ password_last_used=user.get('PasswordLastUsed', '').isoformat()
204
+ if user.get('PasswordLastUsed')
205
+ else None,
206
+ )
207
+
208
+ result = UserDetailsResponse(
209
+ user=user_details,
210
+ attached_policies=attached_policies,
211
+ inline_policies=inline_policies,
212
+ groups=groups,
213
+ access_keys=access_keys,
214
+ )
215
+
216
+ logger.info(f'Successfully retrieved details for user: {user_name}')
217
+ return result
218
+
219
+ except Exception as e:
220
+ error = handle_iam_error(e)
221
+ logger.error(f'Error getting user details: {error}')
222
+ raise error
223
+
224
+
225
+ @mcp.tool()
226
+ async def create_user(
227
+ ctx: CallToolResult,
228
+ user_name: str = Field(description='The name of the new IAM user'),
229
+ path: str = Field(description='The path for the user', default='/'),
230
+ permissions_boundary: Optional[str] = Field(
231
+ description='ARN of the permissions boundary policy', default=None
232
+ ),
233
+ ) -> CreateUserResponse:
234
+ """Create a new IAM user.
235
+
236
+ This tool creates a new IAM user in your AWS account. The user will be created
237
+ without any permissions by default - you'll need to attach policies separately.
238
+
239
+ ## Security Best Practices:
240
+ - Use descriptive user names that indicate the user's role or purpose
241
+ - Set appropriate paths for organizational structure
242
+ - Consider using permissions boundaries to limit maximum permissions
243
+ - Follow the principle of least privilege when assigning permissions later
244
+
245
+ Args:
246
+ ctx: MCP context for error reporting
247
+ user_name: The name of the new IAM user
248
+ path: The path for the user (default: '/')
249
+ permissions_boundary: Optional ARN of the permissions boundary policy
250
+
251
+ Returns:
252
+ CreateUserResponse containing the created user details
253
+ """
254
+ try:
255
+ logger.info(f'Creating IAM user: {user_name}')
256
+
257
+ # Check if server is in read-only mode
258
+ if Context.is_readonly():
259
+ raise IamClientError('Cannot create user: server is running in read-only mode')
260
+
261
+ if not user_name:
262
+ raise IamValidationError('User name is required')
263
+
264
+ iam = get_iam_client()
265
+
266
+ kwargs = {'UserName': user_name, 'Path': path}
267
+
268
+ if permissions_boundary:
269
+ kwargs['PermissionsBoundary'] = permissions_boundary
270
+
271
+ response = iam.create_user(**kwargs)
272
+ user = response['User']
273
+
274
+ user_details = IamUser(
275
+ user_name=user['UserName'],
276
+ user_id=user['UserId'],
277
+ arn=user['Arn'],
278
+ path=user['Path'],
279
+ create_date=user['CreateDate'].isoformat(),
280
+ password_last_used=user.get('PasswordLastUsed').isoformat()
281
+ if user.get('PasswordLastUsed')
282
+ else None,
283
+ )
284
+
285
+ result = CreateUserResponse(
286
+ user=user_details, message=f'Successfully created user: {user_name}'
287
+ )
288
+
289
+ logger.info(f'Successfully created IAM user: {user_name}')
290
+ return result
291
+
292
+ except Exception as e:
293
+ error = handle_iam_error(e)
294
+ logger.error(f'Error creating user: {error}')
295
+ raise error
296
+
297
+
298
+ @mcp.tool()
299
+ async def delete_user(
300
+ user_name: str = Field(description='The name of the IAM user to delete'),
301
+ force: bool = Field(
302
+ description='Force delete user by removing all attached policies, groups, and access keys first',
303
+ default=False,
304
+ ),
305
+ ) -> Dict[str, Any]:
306
+ """Delete an IAM user.
307
+
308
+ Args:
309
+ user_name: The name of the IAM user to delete
310
+ force: If True, removes all attached policies, groups, and access keys first
311
+
312
+ Returns:
313
+ Dictionary containing deletion status
314
+ """
315
+ try:
316
+ # Check if server is in read-only mode
317
+ if Context.is_readonly():
318
+ raise IamClientError('Cannot delete user: server is running in read-only mode')
319
+
320
+ iam = get_iam_client()
321
+
322
+ if force:
323
+ # Remove from all groups
324
+ groups = iam.get_groups_for_user(UserName=user_name)
325
+ for group in groups.get('Groups', []):
326
+ iam.remove_user_from_group(GroupName=group['GroupName'], UserName=user_name)
327
+
328
+ # Detach all managed policies
329
+ attached_policies = iam.list_attached_user_policies(UserName=user_name)
330
+ for policy in attached_policies.get('AttachedPolicies', []):
331
+ iam.detach_user_policy(UserName=user_name, PolicyArn=policy['PolicyArn'])
332
+
333
+ # Delete all inline policies
334
+ inline_policies = iam.list_user_policies(UserName=user_name)
335
+ for policy_name in inline_policies.get('PolicyNames', []):
336
+ iam.delete_user_policy(UserName=user_name, PolicyName=policy_name)
337
+
338
+ # Delete all access keys
339
+ access_keys = iam.list_access_keys(UserName=user_name)
340
+ for key in access_keys.get('AccessKeyMetadata', []):
341
+ iam.delete_access_key(UserName=user_name, AccessKeyId=key['AccessKeyId'])
342
+
343
+ # Delete the user
344
+ iam.delete_user(UserName=user_name)
345
+
346
+ return {'Message': f'Successfully deleted user: {user_name}', 'ForcedCleanup': force}
347
+
348
+ except Exception as e:
349
+ raise handle_iam_error(e)
350
+
351
+
352
+ @mcp.tool()
353
+ async def list_roles(
354
+ path_prefix: Optional[str] = Field(
355
+ description='Path prefix to filter roles (e.g., "/service-role/")', default=None
356
+ ),
357
+ max_items: int = Field(description='Maximum number of roles to return', default=100),
358
+ ) -> Dict[str, Any]:
359
+ """List IAM roles in the account.
360
+
361
+ Args:
362
+ path_prefix: Optional path prefix to filter roles
363
+ max_items: Maximum number of roles to return
364
+
365
+ Returns:
366
+ Dictionary containing list of roles and metadata
367
+ """
368
+ try:
369
+ iam = get_iam_client()
370
+
371
+ kwargs: Dict[str, Any] = {'MaxItems': max_items}
372
+ if path_prefix:
373
+ kwargs['PathPrefix'] = path_prefix
374
+
375
+ response = iam.list_roles(**kwargs)
376
+
377
+ roles = []
378
+ for role in response.get('Roles', []):
379
+ roles.append(
380
+ {
381
+ 'RoleName': role['RoleName'],
382
+ 'RoleId': role['RoleId'],
383
+ 'Arn': role['Arn'],
384
+ 'Path': role['Path'],
385
+ 'CreateDate': role['CreateDate'].isoformat(),
386
+ 'AssumeRolePolicyDocument': role.get('AssumeRolePolicyDocument'),
387
+ 'Description': role.get('Description'),
388
+ 'MaxSessionDuration': role.get('MaxSessionDuration'),
389
+ }
390
+ )
391
+
392
+ return {
393
+ 'Roles': roles,
394
+ 'IsTruncated': response.get('IsTruncated', False),
395
+ 'Marker': response.get('Marker'),
396
+ 'Count': len(roles),
397
+ }
398
+
399
+ except Exception as e:
400
+ raise handle_iam_error(e)
401
+
402
+
403
+ @mcp.tool()
404
+ async def create_role(
405
+ role_name: str = Field(description='The name of the new IAM role'),
406
+ assume_role_policy_document: Union[str, dict] = Field(
407
+ description='The trust policy document in JSON format (string or dict)'
408
+ ),
409
+ path: str = Field(description='The path for the role', default='/'),
410
+ description: Optional[str] = Field(description='Description of the role', default=None),
411
+ max_session_duration: int = Field(
412
+ description='Maximum session duration in seconds (3600-43200)', default=3600
413
+ ),
414
+ permissions_boundary: Optional[str] = Field(
415
+ description='ARN of the permissions boundary policy', default=None
416
+ ),
417
+ ) -> Dict[str, Any]:
418
+ """Create a new IAM role.
419
+
420
+ Args:
421
+ role_name: The name of the new IAM role
422
+ assume_role_policy_document: The trust policy document in JSON format
423
+ path: The path for the role (default: '/')
424
+ description: Optional description of the role
425
+ max_session_duration: Maximum session duration in seconds
426
+ permissions_boundary: Optional ARN of the permissions boundary policy
427
+
428
+ Returns:
429
+ Dictionary containing the created role details
430
+ """
431
+ try:
432
+ # Check if server is in read-only mode
433
+ if Context.is_readonly():
434
+ raise IamClientError('Cannot create role: server is running in read-only mode')
435
+
436
+ iam = get_iam_client()
437
+
438
+ # Handle both string and dict types
439
+ if isinstance(assume_role_policy_document, dict):
440
+ policy_document = json.dumps(assume_role_policy_document)
441
+ else:
442
+ policy_document = assume_role_policy_document
443
+ # Validate JSON
444
+ try:
445
+ json.loads(policy_document)
446
+ except json.JSONDecodeError:
447
+ raise Exception('Invalid JSON in assume_role_policy_document')
448
+
449
+ kwargs = {
450
+ 'RoleName': role_name,
451
+ 'AssumeRolePolicyDocument': policy_document,
452
+ 'Path': path,
453
+ 'MaxSessionDuration': max_session_duration,
454
+ }
455
+
456
+ if description:
457
+ kwargs['Description'] = description
458
+ if permissions_boundary:
459
+ kwargs['PermissionsBoundary'] = permissions_boundary
460
+
461
+ response = iam.create_role(**kwargs)
462
+ role = response['Role']
463
+
464
+ return {
465
+ 'Role': {
466
+ 'RoleName': role['RoleName'],
467
+ 'RoleId': role['RoleId'],
468
+ 'Arn': role['Arn'],
469
+ 'Path': role['Path'],
470
+ 'CreateDate': role['CreateDate'].isoformat(),
471
+ 'AssumeRolePolicyDocument': role.get('AssumeRolePolicyDocument'),
472
+ 'Description': role.get('Description'),
473
+ 'MaxSessionDuration': role.get('MaxSessionDuration'),
474
+ },
475
+ 'Message': f'Successfully created role: {role_name}',
476
+ }
477
+
478
+ except Exception as e:
479
+ raise handle_iam_error(e)
480
+
481
+
482
+ @mcp.tool()
483
+ async def list_policies(
484
+ scope: str = Field(
485
+ description='Scope of policies to list: "All", "AWS", or "Local"', default='Local'
486
+ ),
487
+ only_attached: bool = Field(
488
+ description='Only return policies that are attached to a user, group, or role',
489
+ default=False,
490
+ ),
491
+ path_prefix: Optional[str] = Field(description='Path prefix to filter policies', default=None),
492
+ max_items: int = Field(description='Maximum number of policies to return', default=100),
493
+ ) -> Dict[str, Any]:
494
+ """List IAM policies in the account.
495
+
496
+ Args:
497
+ scope: Scope of policies to list ("All", "AWS", or "Local")
498
+ only_attached: Only return policies that are attached
499
+ path_prefix: Optional path prefix to filter policies
500
+ max_items: Maximum number of policies to return
501
+
502
+ Returns:
503
+ Dictionary containing list of policies and metadata
504
+ """
505
+ try:
506
+ iam = get_iam_client()
507
+
508
+ kwargs = {'Scope': scope, 'OnlyAttached': only_attached, 'MaxItems': max_items}
509
+ if path_prefix:
510
+ kwargs['PathPrefix'] = path_prefix
511
+
512
+ response = iam.list_policies(**kwargs)
513
+
514
+ policies = []
515
+ for policy in response.get('Policies', []):
516
+ policies.append(
517
+ {
518
+ 'PolicyName': policy['PolicyName'],
519
+ 'PolicyId': policy['PolicyId'],
520
+ 'Arn': policy['Arn'],
521
+ 'Path': policy['Path'],
522
+ 'DefaultVersionId': policy['DefaultVersionId'],
523
+ 'AttachmentCount': policy['AttachmentCount'],
524
+ 'PermissionsBoundaryUsageCount': policy.get(
525
+ 'PermissionsBoundaryUsageCount', 0
526
+ ),
527
+ 'IsAttachable': policy['IsAttachable'],
528
+ 'Description': policy.get('Description'),
529
+ 'CreateDate': policy['CreateDate'].isoformat(),
530
+ 'UpdateDate': policy['UpdateDate'].isoformat(),
531
+ }
532
+ )
533
+
534
+ return {
535
+ 'Policies': policies,
536
+ 'IsTruncated': response.get('IsTruncated', False),
537
+ 'Marker': response.get('Marker'),
538
+ 'Count': len(policies),
539
+ }
540
+
541
+ except Exception as e:
542
+ raise handle_iam_error(e)
543
+
544
+
545
+ @mcp.tool()
546
+ async def attach_user_policy(
547
+ user_name: str = Field(description='The name of the IAM user'),
548
+ policy_arn: str = Field(description='The ARN of the policy to attach'),
549
+ ) -> Dict[str, Any]:
550
+ """Attach a managed policy to an IAM user.
551
+
552
+ Args:
553
+ user_name: The name of the IAM user
554
+ policy_arn: The ARN of the policy to attach
555
+
556
+ Returns:
557
+ Dictionary containing attachment status
558
+ """
559
+ try:
560
+ # Check if server is in read-only mode
561
+ if Context.is_readonly():
562
+ raise IamClientError('Cannot attach policy: server is running in read-only mode')
563
+
564
+ iam = get_iam_client()
565
+
566
+ iam.attach_user_policy(UserName=user_name, PolicyArn=policy_arn)
567
+
568
+ return {
569
+ 'Message': f'Successfully attached policy {policy_arn} to user {user_name}',
570
+ 'UserName': user_name,
571
+ 'PolicyArn': policy_arn,
572
+ }
573
+
574
+ except Exception as e:
575
+ raise handle_iam_error(e)
576
+
577
+
578
+ @mcp.tool()
579
+ async def detach_user_policy(
580
+ user_name: str = Field(description='The name of the IAM user'),
581
+ policy_arn: str = Field(description='The ARN of the policy to detach'),
582
+ ) -> Dict[str, Any]:
583
+ """Detach a managed policy from an IAM user.
584
+
585
+ Args:
586
+ user_name: The name of the IAM user
587
+ policy_arn: The ARN of the policy to detach
588
+
589
+ Returns:
590
+ Dictionary containing detachment status
591
+ """
592
+ try:
593
+ # Check if server is in read-only mode
594
+ if Context.is_readonly():
595
+ raise IamClientError('Cannot detach policy: server is running in read-only mode')
596
+
597
+ iam = get_iam_client()
598
+
599
+ iam.detach_user_policy(UserName=user_name, PolicyArn=policy_arn)
600
+
601
+ return {
602
+ 'Message': f'Successfully detached policy {policy_arn} from user {user_name}',
603
+ 'UserName': user_name,
604
+ 'PolicyArn': policy_arn,
605
+ }
606
+
607
+ except Exception as e:
608
+ raise handle_iam_error(e)
609
+
610
+
611
+ @mcp.tool()
612
+ async def create_access_key(
613
+ user_name: str = Field(description='The name of the IAM user'),
614
+ ) -> Dict[str, Any]:
615
+ """Create a new access key for an IAM user.
616
+
617
+ Args:
618
+ user_name: The name of the IAM user
619
+
620
+ Returns:
621
+ Dictionary containing the new access key details
622
+ """
623
+ try:
624
+ # Check if server is in read-only mode
625
+ if Context.is_readonly():
626
+ raise IamClientError('Cannot create access key: server is running in read-only mode')
627
+
628
+ iam = get_iam_client()
629
+
630
+ response = iam.create_access_key(UserName=user_name)
631
+ access_key = response['AccessKey']
632
+
633
+ return {
634
+ 'AccessKey': {
635
+ 'AccessKeyId': access_key['AccessKeyId'],
636
+ 'SecretAccessKey': access_key['SecretAccessKey'],
637
+ 'Status': access_key['Status'],
638
+ 'UserName': access_key['UserName'],
639
+ 'CreateDate': access_key['CreateDate'].isoformat(),
640
+ },
641
+ 'Message': f'Successfully created access key for user: {user_name}',
642
+ 'Warning': 'Store the SecretAccessKey securely - it cannot be retrieved again!',
643
+ }
644
+
645
+ except Exception as e:
646
+ raise handle_iam_error(e)
647
+
648
+
649
+ @mcp.tool()
650
+ async def delete_access_key(
651
+ user_name: str = Field(description='The name of the IAM user'),
652
+ access_key_id: str = Field(description='The access key ID to delete'),
653
+ ) -> Dict[str, Any]:
654
+ """Delete an access key for an IAM user.
655
+
656
+ Args:
657
+ user_name: The name of the IAM user
658
+ access_key_id: The access key ID to delete
659
+
660
+ Returns:
661
+ Dictionary containing deletion status
662
+ """
663
+ try:
664
+ # Check if server is in read-only mode
665
+ if Context.is_readonly():
666
+ raise IamClientError('Cannot delete access key: server is running in read-only mode')
667
+
668
+ iam = get_iam_client()
669
+
670
+ iam.delete_access_key(UserName=user_name, AccessKeyId=access_key_id)
671
+
672
+ return {
673
+ 'Message': f'Successfully deleted access key {access_key_id} for user {user_name}',
674
+ 'UserName': user_name,
675
+ 'AccessKeyId': access_key_id,
676
+ }
677
+
678
+ except Exception as e:
679
+ raise handle_iam_error(e)
680
+
681
+
682
+ @mcp.tool()
683
+ async def simulate_principal_policy(
684
+ policy_source_arn: str = Field(description='ARN of the user or role to simulate'),
685
+ action_names: List[str] = Field(description='List of actions to simulate'),
686
+ resource_arns: Optional[List[str]] = Field(
687
+ description='List of resource ARNs to test against', default=None
688
+ ),
689
+ context_entries: Optional[Dict[str, str]] = Field(
690
+ description='Context entries for the simulation', default=None
691
+ ),
692
+ ) -> Dict[str, Any]:
693
+ """Simulate IAM policy evaluation for a principal.
694
+
695
+ Args:
696
+ policy_source_arn: ARN of the user or role to simulate
697
+ action_names: List of actions to simulate
698
+ resource_arns: Optional list of resource ARNs to test against
699
+ context_entries: Optional context entries for the simulation
700
+
701
+ Returns:
702
+ Dictionary containing simulation results
703
+ """
704
+ try:
705
+ iam = get_iam_client()
706
+
707
+ kwargs = {'PolicySourceArn': policy_source_arn, 'ActionNames': action_names}
708
+
709
+ if resource_arns:
710
+ kwargs['ResourceArns'] = resource_arns
711
+ if context_entries:
712
+ kwargs['ContextEntries'] = [
713
+ {'ContextKeyName': k, 'ContextKeyValues': [v]} for k, v in context_entries.items()
714
+ ]
715
+
716
+ response = iam.simulate_principal_policy(**kwargs)
717
+
718
+ results = []
719
+ for result in response.get('EvaluationResults', []):
720
+ results.append(
721
+ {
722
+ 'EvalActionName': result['EvalActionName'],
723
+ 'EvalResourceName': result.get('EvalResourceName', '*'),
724
+ 'EvalDecision': result['EvalDecision'],
725
+ 'MatchedStatements': result.get('MatchedStatements', []),
726
+ 'MissingContextValues': result.get('MissingContextValues', []),
727
+ }
728
+ )
729
+
730
+ return {
731
+ 'EvaluationResults': results,
732
+ 'IsTruncated': response.get('IsTruncated', False),
733
+ 'Marker': response.get('Marker'),
734
+ 'PolicySourceArn': policy_source_arn,
735
+ }
736
+
737
+ except Exception as e:
738
+ raise handle_iam_error(e)
739
+
740
+
741
+ def main():
742
+ """Run the MCP server with CLI argument support."""
743
+ parser = argparse.ArgumentParser(
744
+ description='An AWS Labs Model Context Protocol (MCP) server for comprehensive AWS IAM management'
745
+ )
746
+ parser.add_argument(
747
+ '--readonly',
748
+ action=argparse.BooleanOptionalAction,
749
+ help='Prevents the MCP server from performing mutating operations',
750
+ default=False,
751
+ )
752
+ parser.add_argument('--region', help='AWS region to use for operations')
753
+
754
+ args = parser.parse_args()
755
+
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
+
762
+ if args.readonly:
763
+ logger.info('Running in read-only mode - mutating operations will be disabled')
764
+
765
+ mcp.run()
766
+
767
+
768
+ if __name__ == '__main__':
769
+ main()