Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,39 @@
# v5.3.0
## Key Features
### Model Context Protocol (MCP) Workbench
LISA now includes a comprehensive MCP Workbench that enables administrators to create, test, manage and host custom MCP tools directly within LISA.

#### MCP Tool Development
- **Custom Tool Creation**: Administrators can create and edit custom MCP tools using a built-in code editor with syntax highlighting
- **Tool Testing Environment**: Integrated testing capabilities for validating MCP tools before enterprise rollout
- **Template-Based Development**: Pre-built tempslate and examples to accelerate tool development
- **MCP file hosting support**: Administrators can upload MCP tool code directly to S3. The MCP Workbench connection will automatically host this tool for use
- **Improved Authentication**: Enhanced authentication mechanisms for MCP server connections, if users specify `{LISA_BEARER_TOKEN}` in the header field, LISA will populate this with the users active token. This is important for proxying calls to internally hosted servers that use the same authentication mechanisms as LISA

#### Administrative Control
- **Tool Management**: Administrators can manage and configure the MCP workbench capabilities for their organization
- **IDP Group Locking**: MCP connections can now be locked down to specific Identity Provider (IdP) groups for enhanced security

### Enhanced Model Control
- **Custom API Key Support**: Support for handling custom API keys for third-party models added to Model Management

### Mermaid Diagram Sanitization
- **Security Enhancement**: Implemented sanitization for Mermaid diagrams to prevent potential security vulnerabilities
- **Safe Rendering**: Ensures that Mermaid diagrams are rendered safely without executing malicious code

## What's Next?
We'll be launching broader MCP tool hosting capabilities in an upcoming LISA release.

## Acknowledgements
* @bedanley
* @estohlmann
* @jmharold
* @dustins
* @jonleeh
* @drduhe

**Full Changelog**: https://github.com/awslabs/LISA/compare/v5.2.0...v5.3.0

# v5.2.0
## Key Features
### Model Context Protocol (MCP) Enhancements
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
5.2.0
5.3.0
10 changes: 7 additions & 3 deletions ecs_model_deployer/src/lib/ecs-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,19 @@ export class EcsModel extends Construct {
const { config, modelConfig, securityGroup, vpc, subnetSelection } = props;

const modelCluster = new ECSCluster(scope, `${id}-ECC`, {
identifier: getModelIdentifier(modelConfig),
config,
ecsConfig: {
tasks: {
[getModelIdentifier(modelConfig)]: {
containerConfig: modelConfig.containerConfig,
environment: this.getEnvironmentVariables(config, modelConfig),
}
},
amiHardwareType: AmiHardwareType.GPU,
autoScalingConfig: modelConfig.autoScalingConfig,
buildArgs: this.getBuildArguments(config, modelConfig),
containerConfig: modelConfig.containerConfig,
containerMemoryBuffer: CONTAINER_MEMORY_BUFFER,
environment: this.getEnvironmentVariables(config, modelConfig),
identifier: getModelIdentifier(modelConfig),
instanceType: modelConfig.instanceType,
internetFacing: false,
loadBalancerConfig: modelConfig.loadBalancerConfig,
Expand Down
400 changes: 206 additions & 194 deletions ecs_model_deployer/src/lib/ecsCluster.ts

Large diffs are not rendered by default.

17 changes: 2 additions & 15 deletions lambda/authorizer/lambda_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@
import ssl
from datetime import datetime
from functools import cache
from typing import Any, Dict, Optional
from typing import Any, Dict

import boto3
import create_env_variables # noqa: F401
import jwt
import requests
from botocore.exceptions import ClientError
from utilities.common_functions import authorization_wrapper, get_id_token, retry_config
from utilities.common_functions import authorization_wrapper, get_id_token, get_property_path, retry_config

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -187,19 +187,6 @@ def is_user(jwt_data: dict[str, Any], user_group: str, jwt_groups_property: str)
return user_group in (get_property_path(jwt_data, jwt_groups_property) or [])


def get_property_path(data: dict[str, Any], property_path: str) -> Optional[Any]:
"""Get the value represented by a property path."""
props = property_path.split(".")
current_node = data
for prop in props:
if prop in current_node:
current_node = current_node[prop]
else:
return None

return current_node


def find_jwt_username(jwt_data: dict[str, str]) -> str:
"""Find the username in the JWT. If the key 'username' doesn't exist, return 'sub', which will be a UUID"""
username = None
Expand Down
39 changes: 38 additions & 1 deletion lambda/configuration/lambda_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
import boto3
import create_env_variables # noqa: F401
from botocore.exceptions import ClientError
from utilities.common_functions import api_wrapper, retry_config
from mcp_server.models import McpServerModel
from mcp_workbench.lambda_functions import MCPWORKBENCH_UUID
from utilities.common_functions import api_wrapper, get_property_path, retry_config

logger = logging.getLogger(__name__)

Expand All @@ -36,6 +38,10 @@ def get_configuration(event: dict, context: dict) -> Dict[str, Any]:
"""List configuration entries by configScope from DynamoDB."""
config_scope = event["queryStringParameters"]["configScope"]

return _get_configurations(config_scope)


def _get_configurations(config_scope: str) -> dict[str, Any]:
response = {}
try:
response = table.query(
Expand All @@ -49,6 +55,7 @@ def get_configuration(event: dict, context: dict) -> Dict[str, Any]:
logger.warning(f"No record found with session id: {config_scope}")
else:
logger.exception("Error fetching session")

return response.get("Items", {}) # type: ignore [no-any-return]


Expand All @@ -59,7 +66,37 @@ def update_configuration(event: dict, context: dict) -> None:
body = json.loads(event["body"], parse_float=Decimal)
body["created_at"] = str(Decimal(time.time()))

# check if showMcpWorkbench configuration changed
old_configurations = _get_configurations(body["configScope"])
old_configuration = old_configurations[0] if old_configurations else {}
check_show_mcp_workbench(body, old_configuration)

try:
table.put_item(Item=body)
except ClientError:
logger.exception("Error updating session in DynamoDB")


def check_show_mcp_workbench(body, old_configuration):
old_show_mcp_value = get_property_path(old_configuration, "configuration.enabledComponents.showMcpWorkbench")
new_show_mcp_value = get_property_path(body, "configuration.enabledComponents.showMcpWorkbench")

# check if the value changed
if old_show_mcp_value != new_show_mcp_value:
from mcp_server.lambda_functions import table as mcp_servers_table

if new_show_mcp_value:
mcp_server_model = McpServerModel(
id=MCPWORKBENCH_UUID,
owner="lisa:public",
name="MCP Workbench",
description="MCP Workbench Tools",
customHeaders={"Authorization": "Bearer {LISA_BEARER_TOKEN}"},
url=f"{os.getenv('FASTAPI_ENDPOINT')}/v2/mcp/",
)

# Insert the new mcp server item into the DynamoDB table
mcp_servers_table.put_item(Item=mcp_server_model.model_dump(exclude_none=True))
else:
logger.info("Deleting mcp server MCP Workbench Server")
mcp_servers_table.delete_item(Key={"id": MCPWORKBENCH_UUID, "owner": "lisa:public"})
54 changes: 54 additions & 0 deletions lambda/management_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import json
import logging
import os
from datetime import datetime
from typing import Any, Dict

import boto3
Expand All @@ -28,6 +29,7 @@
logger.setLevel(logging.INFO)

secrets_manager = boto3.client("secretsmanager", region_name=os.environ["AWS_REGION"], config=retry_config)
events_client = boto3.client("events", region_name=os.environ["AWS_REGION"], config=retry_config)


def handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
Expand Down Expand Up @@ -169,11 +171,63 @@ def finish_secret(secret_arn: str, token: str) -> None:

logger.info(f"Successfully finished rotation - version {token} is now current")

# Publish event to EventBridge after successful rotation
publish_rotation_event(secret_arn, token, current_version)

except ClientError as e:
logger.error(f"Error finishing secret rotation: {e}")
raise


def publish_rotation_event(secret_arn: str, new_version: str, old_version: str) -> None:
"""
Publish a management key rotation event to EventBridge.
"""
event_bus_name = os.environ.get("EVENT_BUS_NAME")
if not event_bus_name:
logger.warning("EVENT_BUS_NAME environment variable not set, skipping event publication")
return

try:
# Extract secret name from ARN for cleaner event data
secret_name = secret_arn.split(":")[-1]

event_detail = {
"secretArn": secret_arn,
"secretName": secret_name,
"newVersionId": new_version,
"oldVersionId": old_version,
"rotationTimestamp": datetime.utcnow().isoformat() + "Z",
"rotationType": "management-key",
}

# Publish event to EventBridge
response = events_client.put_events(
Entries=[
{
"Source": "lisa.management-key",
"DetailType": "Management Key Rotated",
"Detail": json.dumps(event_detail),
"EventBusName": event_bus_name,
"Time": datetime.utcnow(),
}
]
)

if response["FailedEntryCount"] > 0:
logger.error(f"Failed to publish event: {response['Entries']}")
raise Exception("Failed to publish rotation event to EventBridge")

logger.info(f"Successfully published rotation event for secret {secret_name} to EventBridge")

except ClientError as e:
logger.error(f"Error publishing rotation event: {e}")
# Don't raise the exception as event publishing failure shouldn't fail the rotation
except Exception as e:
logger.error(f"Unexpected error publishing rotation event: {e}")
# Don't raise the exception as event publishing failure shouldn't fail the rotation


# Legacy function for backward compatibility
def rotate_management_key(event: dict, ctx: dict) -> None:
"""Legacy rotation function - deprecated, use handler instead."""
Expand Down
Loading
Loading