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.
- awslabs/iam_mcp_server/__init__.py +1 -1
- awslabs/iam_mcp_server/context.py +5 -0
- awslabs/iam_mcp_server/models.py +93 -0
- awslabs/iam_mcp_server/server.py +844 -17
- {awslabs_iam_mcp_server-1.0.1.dist-info → awslabs_iam_mcp_server-1.0.3.dist-info}/METADATA +223 -8
- awslabs_iam_mcp_server-1.0.3.dist-info/RECORD +13 -0
- awslabs_iam_mcp_server-1.0.1.dist-info/RECORD +0 -13
- {awslabs_iam_mcp_server-1.0.1.dist-info → awslabs_iam_mcp_server-1.0.3.dist-info}/WHEEL +0 -0
- {awslabs_iam_mcp_server-1.0.1.dist-info → awslabs_iam_mcp_server-1.0.3.dist-info}/entry_points.txt +0 -0
- {awslabs_iam_mcp_server-1.0.1.dist-info → awslabs_iam_mcp_server-1.0.3.dist-info}/licenses/LICENSE +0 -0
- {awslabs_iam_mcp_server-1.0.1.dist-info → awslabs_iam_mcp_server-1.0.3.dist-info}/licenses/NOTICE +0 -0
awslabs/iam_mcp_server/server.py
CHANGED
|
@@ -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. **
|
|
49
|
-
5. **
|
|
50
|
-
6. **
|
|
51
|
-
7. **
|
|
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.
|
|
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.
|
|
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=
|
|
749
|
-
help='
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
|