diff --git a/CHANGELOG.md b/CHANGELOG.md index 323fbe16c..64a67e4b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/VERSION b/VERSION index 91ff57278..03f488b07 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.2.0 +5.3.0 diff --git a/ecs_model_deployer/src/lib/ecs-model.ts b/ecs_model_deployer/src/lib/ecs-model.ts index 27aa8ba94..8e6b32b8f 100644 --- a/ecs_model_deployer/src/lib/ecs-model.ts +++ b/ecs_model_deployer/src/lib/ecs-model.ts @@ -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, diff --git a/ecs_model_deployer/src/lib/ecsCluster.ts b/ecs_model_deployer/src/lib/ecsCluster.ts index 81f55bee1..45b95cd95 100644 --- a/ecs_model_deployer/src/lib/ecsCluster.ts +++ b/ecs_model_deployer/src/lib/ecsCluster.ts @@ -55,6 +55,7 @@ import { CodeFactory } from '../../../lib/util'; * @property {string} executionRoleName? - The role used for executing the task */ type ECSClusterProps = { + identifier: string; ecsConfig: ECSConfig; securityGroup: ISecurityGroup; vpc: IVpc; @@ -84,17 +85,17 @@ export class ECSCluster extends Construct { */ constructor (scope: Construct, id: string, props: ECSClusterProps) { super(scope, id); - const { config, vpc, securityGroup, ecsConfig, subnetSelection, taskRoleName, executionRoleName } = props; + const { identifier, config, vpc, securityGroup, ecsConfig, subnetSelection, taskRoleName, executionRoleName } = props; // Create ECS cluster - const cluster = new Cluster(this, createCdkId([ecsConfig.identifier, 'Cl']), { - clusterName: createCdkId([config.deploymentName, ecsConfig.identifier], 32, 2), + const cluster = new Cluster(this, createCdkId([identifier, 'Cl']), { + clusterName: createCdkId([config.deploymentName, identifier], 32, 2), vpc: vpc, containerInsightsV2: !config.region?.includes('iso') ? ContainerInsights.ENABLED : ContainerInsights.DISABLED, }); // Create auto scaling group - const autoScalingGroup = cluster.addCapacity(createCdkId([ecsConfig.identifier, 'ASG']), { + const autoScalingGroup = cluster.addCapacity(createCdkId([identifier, 'ASG']), { vpcSubnets: subnetSelection, instanceType: new InstanceType(ecsConfig.instanceType), machineImage: EcsOptimizedImage.amazonLinux2(ecsConfig.amiHardwareType), @@ -120,207 +121,218 @@ export class ECSCluster extends Construct { value: autoScalingGroup.autoScalingGroupName, }); - const environment = ecsConfig.environment; - const volumes: Volume[] = []; - const mountPoints: MountPoint[] = []; - - // If NVMe drive available, mount and use it - if (Ec2Metadata.get(ecsConfig.instanceType).nvmePath) { - // EC2 user data to mount ephemeral NVMe drive - const MOUNT_PATH = config.nvmeHostMountPath ?? '/nvme'; - const NVME_PATH = Ec2Metadata.get(ecsConfig.instanceType).nvmePath; - /* eslint-disable no-useless-escape */ - const rawUserData = `#!/bin/bash - set -e - # Check if NVMe is already formatted - if ! blkid ${NVME_PATH}; then - mkfs.xfs ${NVME_PATH} - fi - - mkdir -p ${MOUNT_PATH} - mount ${NVME_PATH} ${MOUNT_PATH} - - # Add to fstab if not already present - if ! grep -q "${NVME_PATH}" /etc/fstab; then - echo ${NVME_PATH} ${MOUNT_PATH} xfs defaults,nofail 0 2 >> /etc/fstab - fi - - # Update Docker root location and restart Docker service - mkdir -p ${MOUNT_PATH}/docker - echo '{\"data-root\": \"${MOUNT_PATH}/docker\"}' | tee /etc/docker/daemon.json - systemctl restart docker - `; - /* eslint-enable no-useless-escape */ - autoScalingGroup.addUserData(rawUserData); - - // Create mount point for container - const sourceVolume = 'nvme'; - const host: Host = { sourcePath: config.nvmeHostMountPath ?? '/nvme' }; - const nvmeVolume: Volume = { name: sourceVolume, host: host }; - const nvmeMountPoint: MountPoint = { - sourceVolume: sourceVolume, - containerPath: config.nvmeContainerMountPath ?? '/nvme', - readOnly: false, + // we want to set these based on the task created but currently the ECSCluster for model + // will only create one task, so grab these values during creation so we can set the properties + // on this class + let container; + let taskRole; + let endpointUrl; + + Object.entries(ecsConfig.tasks).forEach(([, taskDefinition]) => { + const environment = taskDefinition.environment; + + const volumes: Volume[] = []; + const mountPoints: MountPoint[] = []; + + // If NVMe drive available, mount and use it + if (Ec2Metadata.get(ecsConfig.instanceType).nvmePath) { + // EC2 user data to mount ephemeral NVMe drive + const MOUNT_PATH = config.nvmeHostMountPath ?? '/nvme'; + const NVME_PATH = Ec2Metadata.get(ecsConfig.instanceType).nvmePath; + /* eslint-disable no-useless-escape */ + const rawUserData = `#!/bin/bash + set -e + # Check if NVMe is already formatted + if ! blkid ${NVME_PATH}; then + mkfs.xfs ${NVME_PATH} + fi + + mkdir -p ${MOUNT_PATH} + mount ${NVME_PATH} ${MOUNT_PATH} + + # Add to fstab if not already present + if ! grep -q "${NVME_PATH}" /etc/fstab; then + echo ${NVME_PATH} ${MOUNT_PATH} xfs defaults,nofail 0 2 >> /etc/fstab + fi + + # Update Docker root location and restart Docker service + mkdir -p ${MOUNT_PATH}/docker + echo '{\"data-root\": \"${MOUNT_PATH}/docker\"}' | tee /etc/docker/daemon.json + systemctl restart docker + `; + /* eslint-enable no-useless-escape */ + autoScalingGroup.addUserData(rawUserData); + + // Create mount point for container + const sourceVolume = 'nvme'; + const host: Host = { sourcePath: config.nvmeHostMountPath ?? '/nvme' }; + const nvmeVolume: Volume = { name: sourceVolume, host: host }; + const nvmeMountPoint: MountPoint = { + sourceVolume: sourceVolume, + containerPath: config.nvmeContainerMountPath ?? '/nvme', + readOnly: false, + }; + volumes.push(nvmeVolume); + mountPoints.push(nvmeMountPoint); + } + + // Add permissions to use SSM in dev environment for EC2 debugging purposes only + if (config.deploymentStage === 'dev') { + autoScalingGroup.role.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMFullAccess')); + } + + if (config.region?.includes('iso')) { + const pkiSourceVolume = 'pki'; + const pkiHost: Host = { sourcePath: '/etc/pki' }; + const pkiVolume: Volume = { name: pkiSourceVolume, host: pkiHost }; + const pkiMountPoint: MountPoint = { + sourceVolume: pkiSourceVolume, + containerPath: '/etc/pki', + readOnly: false, + }; + volumes.push(pkiVolume); + mountPoints.push(pkiMountPoint); + // Requires mount point /etc/pki from host + environment.SSL_CERT_DIR = '/etc/pki/tls/certs'; + environment.SSL_CERT_FILE = config.certificateAuthorityBundle ?? ''; + environment.REQUESTS_CA_BUNDLE = config.certificateAuthorityBundle ?? ''; + environment.AWS_CA_BUNDLE = config.certificateAuthorityBundle ?? ''; + environment.CURL_CA_BUNDLE = config.certificateAuthorityBundle ?? ''; + } + + const roleId = identifier; + const taskRole = taskRoleName ? + Role.fromRoleName(this, createCdkId([config.deploymentName, roleId]), taskRoleName) : + this.createTaskRole(config.deploymentName ?? '', config.deploymentPrefix, roleId); + + // Create ECS task definition + const ec2TaskDefinition = new Ec2TaskDefinition(this, createCdkId([roleId, 'Ec2TaskDefinition']), { + family: createCdkId([config.deploymentName, roleId], 32, 2), + volumes: volumes, + taskRole, + ...(executionRoleName && { executionRole: Role.fromRoleName(this, createCdkId([config.deploymentName, roleId, 'EX']), executionRoleName) }), + }); + + // Add container to task definition + const containerHealthCheckConfig = taskDefinition.containerConfig.healthCheckConfig; + const containerHealthCheck: HealthCheck = { + command: containerHealthCheckConfig.command, + interval: Duration.seconds(containerHealthCheckConfig.interval), + startPeriod: Duration.seconds(containerHealthCheckConfig.startPeriod), + timeout: Duration.seconds(containerHealthCheckConfig.timeout), + retries: containerHealthCheckConfig.retries, }; - volumes.push(nvmeVolume); - mountPoints.push(nvmeMountPoint); - } - - // Add permissions to use SSM in dev environment for EC2 debugging purposes only - if (config.deploymentStage === 'dev') { - autoScalingGroup.role.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMFullAccess')); - } - - if (config.region?.includes('iso')) { - const pkiSourceVolume = 'pki'; - const pkiHost: Host = { sourcePath: '/etc/pki' }; - const pkiVolume: Volume = { name: pkiSourceVolume, host: pkiHost }; - const pkiMountPoint: MountPoint = { - sourceVolume: pkiSourceVolume, - containerPath: '/etc/pki', - readOnly: false, - }; - volumes.push(pkiVolume); - mountPoints.push(pkiMountPoint); - // Requires mount point /etc/pki from host - environment.SSL_CERT_DIR = '/etc/pki/tls/certs'; - environment.SSL_CERT_FILE = config.certificateAuthorityBundle ?? ''; - environment.REQUESTS_CA_BUNDLE = config.certificateAuthorityBundle ?? ''; - environment.AWS_CA_BUNDLE = config.certificateAuthorityBundle ?? ''; - environment.CURL_CA_BUNDLE = config.certificateAuthorityBundle ?? ''; - } - - const roleId = ecsConfig.identifier; - const taskRole = taskRoleName ? - Role.fromRoleName(this, createCdkId([config.deploymentName, roleId]), taskRoleName) : - this.createTaskRole(config.deploymentName ?? '', config.deploymentPrefix, roleId); - - // Create ECS task definition - const taskDefinition = new Ec2TaskDefinition(this, createCdkId([roleId, 'Ec2TaskDefinition']), { - family: createCdkId([config.deploymentName, roleId], 32, 2), - volumes: volumes, - taskRole, - ...(executionRoleName && { executionRole: Role.fromRoleName(this, createCdkId([config.deploymentName, roleId, 'EX']), executionRoleName) }), - }); - // Add container to task definition - const containerHealthCheckConfig = ecsConfig.containerConfig.healthCheckConfig; - const containerHealthCheck: HealthCheck = { - command: containerHealthCheckConfig.command, - interval: Duration.seconds(containerHealthCheckConfig.interval), - startPeriod: Duration.seconds(containerHealthCheckConfig.startPeriod), - timeout: Duration.seconds(containerHealthCheckConfig.timeout), - retries: containerHealthCheckConfig.retries, - }; - - const linuxParameters = - ecsConfig.containerConfig.sharedMemorySize > 0 - ? new LinuxParameters(this, createCdkId([ecsConfig.identifier, 'LinuxParameters']), { - sharedMemorySize: ecsConfig.containerConfig.sharedMemorySize, - }) - : undefined; - - const image = CodeFactory.createImage(ecsConfig.containerConfig.image, this, ecsConfig.identifier, ecsConfig.buildArgs); - const container = taskDefinition.addContainer(createCdkId([ecsConfig.identifier, 'Container']), { - containerName: createCdkId([config.deploymentName, ecsConfig.identifier], 32, 2), - image, - environment, - logging: LogDriver.awsLogs({ streamPrefix: ecsConfig.identifier }), - gpuCount: Ec2Metadata.get(ecsConfig.instanceType).gpuCount, - memoryReservationMiB: Ec2Metadata.get(ecsConfig.instanceType).memory - ecsConfig.containerMemoryBuffer, - portMappings: [{ hostPort: 80, containerPort: 8080, protocol: Protocol.TCP }], - healthCheck: containerHealthCheck, - // Model containers need to run with privileged set to true - privileged: ecsConfig.amiHardwareType === AmiHardwareType.GPU, - ...(linuxParameters && { linuxParameters }), - }); - container.addMountPoints(...mountPoints); - - // Create ECS service - const serviceProps: Ec2ServiceProps = { - cluster: cluster, - daemon: true, - serviceName: createCdkId([config.deploymentName, ecsConfig.identifier], 32, 2), - taskDefinition: taskDefinition, - circuitBreaker: !config.region?.includes('iso') ? { rollback: true } : undefined, - }; - - const service = new Ec2Service(this, createCdkId([ecsConfig.identifier, 'Ec2Svc']), serviceProps); - - service.node.addDependency(autoScalingGroup); - - // Create application load balancer - const loadBalancer = new ApplicationLoadBalancer(this, createCdkId([ecsConfig.identifier, 'ALB']), { - deletionProtection: config.removalPolicy !== RemovalPolicy.DESTROY, - internetFacing: false, - loadBalancerName: createCdkId([config.deploymentName, ecsConfig.identifier], 32, 2).toLowerCase(), - dropInvalidHeaderFields: true, - securityGroup, - vpc, - vpcSubnets: subnetSelection, - idleTimeout: Duration.seconds(600) - }); - - // Add listener - const listenerProps: BaseApplicationListenerProps = { - port: 80, - open: false, - certificates: undefined, - }; + const linuxParameters = + taskDefinition.containerConfig.sharedMemorySize > 0 + ? new LinuxParameters(this, createCdkId([identifier, 'LinuxParameters']), { + sharedMemorySize: taskDefinition.containerConfig.sharedMemorySize, + }) + : undefined; + + const image = CodeFactory.createImage(taskDefinition.containerConfig.image, this, identifier, ecsConfig.buildArgs); + const container = ec2TaskDefinition.addContainer(createCdkId([identifier, 'Container']), { + containerName: createCdkId([config.deploymentName, identifier], 32, 2), + image, + environment, + logging: LogDriver.awsLogs({ streamPrefix: identifier }), + gpuCount: Ec2Metadata.get(ecsConfig.instanceType).gpuCount, + memoryReservationMiB: Ec2Metadata.get(ecsConfig.instanceType).memory - ecsConfig.containerMemoryBuffer, + portMappings: [{ hostPort: 80, containerPort: 8080, protocol: Protocol.TCP }], + healthCheck: containerHealthCheck, + // Model containers need to run with privileged set to true + privileged: ecsConfig.amiHardwareType === AmiHardwareType.GPU, + ...(linuxParameters && { linuxParameters }), + }); + container.addMountPoints(...mountPoints); + + // Create ECS service + const serviceProps: Ec2ServiceProps = { + cluster: cluster, + daemon: true, + serviceName: createCdkId([config.deploymentName, identifier], 32, 2), + taskDefinition: ec2TaskDefinition, + circuitBreaker: !config.region?.includes('iso') ? { rollback: true } : undefined, + }; - const listener = loadBalancer.addListener( - createCdkId([ecsConfig.identifier, 'ApplicationListener']), - listenerProps, - ); - const protocol = 'http'; - - // Add targets - const loadBalancerHealthCheckConfig = ecsConfig.loadBalancerConfig.healthCheckConfig; - const targetGroup = listener.addTargets(createCdkId([ecsConfig.identifier, 'TgtGrp']), { - targetGroupName: createCdkId([config.deploymentName, ecsConfig.identifier], 32, 2).toLowerCase(), - healthCheck: { - path: loadBalancerHealthCheckConfig.path, - interval: Duration.seconds(loadBalancerHealthCheckConfig.interval), - timeout: Duration.seconds(loadBalancerHealthCheckConfig.timeout), - healthyThresholdCount: loadBalancerHealthCheckConfig.healthyThresholdCount, - unhealthyThresholdCount: loadBalancerHealthCheckConfig.unhealthyThresholdCount, - }, - port: 80, - targets: [service], - }); + const service = new Ec2Service(this, createCdkId([identifier, 'Ec2Svc']), serviceProps); + + service.node.addDependency(autoScalingGroup); + + // Create application load balancer + const loadBalancer = new ApplicationLoadBalancer(this, createCdkId([identifier, 'ALB']), { + deletionProtection: config.removalPolicy !== RemovalPolicy.DESTROY, + internetFacing: false, + loadBalancerName: createCdkId([config.deploymentName, identifier], 32, 2).toLowerCase(), + dropInvalidHeaderFields: true, + securityGroup, + vpc, + vpcSubnets: subnetSelection, + idleTimeout: Duration.seconds(600) + }); + + // Add listener + const listenerProps: BaseApplicationListenerProps = { + port: 80, + open: false, + certificates: undefined, + }; - // ALB metric for ASG to use for auto scaling EC2 instances - // TODO: Update this to step scaling for embedding models?? - const requestCountPerTargetMetric = new Metric({ - metricName: ecsConfig.autoScalingConfig.metricConfig.albMetricName, - namespace: 'AWS/ApplicationELB', - dimensionsMap: { - TargetGroup: targetGroup.targetGroupFullName, - LoadBalancer: loadBalancer.loadBalancerFullName, - }, - statistic: Stats.SAMPLE_COUNT, - period: Duration.seconds(ecsConfig.autoScalingConfig.metricConfig.duration), - }); + const listener = loadBalancer.addListener( + createCdkId([identifier, 'ApplicationListener']), + listenerProps, + ); + const protocol = 'http'; + + // Add targets + const loadBalancerHealthCheckConfig = ecsConfig.loadBalancerConfig.healthCheckConfig; + const targetGroup = listener.addTargets(createCdkId([identifier, 'TgtGrp']), { + targetGroupName: createCdkId([config.deploymentName, identifier], 32, 2).toLowerCase(), + healthCheck: { + path: loadBalancerHealthCheckConfig.path, + interval: Duration.seconds(loadBalancerHealthCheckConfig.interval), + timeout: Duration.seconds(loadBalancerHealthCheckConfig.timeout), + healthyThresholdCount: loadBalancerHealthCheckConfig.healthyThresholdCount, + unhealthyThresholdCount: loadBalancerHealthCheckConfig.unhealthyThresholdCount, + }, + port: 80, + targets: [service], + }); + + // ALB metric for ASG to use for auto scaling EC2 instances + // TODO: Update this to step scaling for embedding models?? + const requestCountPerTargetMetric = new Metric({ + metricName: ecsConfig.autoScalingConfig.metricConfig.albMetricName, + namespace: 'AWS/ApplicationELB', + dimensionsMap: { + TargetGroup: targetGroup.targetGroupFullName, + LoadBalancer: loadBalancer.loadBalancerFullName, + }, + statistic: Stats.SAMPLE_COUNT, + period: Duration.seconds(ecsConfig.autoScalingConfig.metricConfig.duration), + }); - // Create hook to scale on ALB metric count exceeding thresholds - autoScalingGroup.scaleToTrackMetric(createCdkId([ecsConfig.identifier, 'ScalingPolicy']), { - metric: requestCountPerTargetMetric, - targetValue: ecsConfig.autoScalingConfig.metricConfig.targetValue, - estimatedInstanceWarmup: Duration.seconds(ecsConfig.autoScalingConfig.metricConfig.duration), - }); + // Create hook to scale on ALB metric count exceeding thresholds + autoScalingGroup.scaleToTrackMetric(createCdkId([identifier, 'ScalingPolicy']), { + metric: requestCountPerTargetMetric, + targetValue: ecsConfig.autoScalingConfig.metricConfig.targetValue, + estimatedInstanceWarmup: Duration.seconds(ecsConfig.autoScalingConfig.metricConfig.duration), + }); - const domain = loadBalancer.loadBalancerDnsName; + const domain = loadBalancer.loadBalancerDnsName; - this.endpointUrl = `${protocol}://${domain}`; + endpointUrl = `${protocol}://${domain}`; - new CfnOutput(this, 'modelEndpointurl', { - key: 'modelEndpointUrl', - value: this.endpointUrl, + new CfnOutput(this, 'modelEndpointurl', { + key: 'modelEndpointUrl', + value: this.endpointUrl, + }); }); // Update - this.container = container; - this.taskRole = taskRole; + this.endpointUrl = endpointUrl!; + this.container = container!; + this.taskRole = taskRole!; } createTaskRole (deploymentName: string, deploymentPrefix: string | undefined, roleId: string): IRole { diff --git a/lambda/authorizer/lambda_functions.py b/lambda/authorizer/lambda_functions.py index c5ed40650..ed6716e89 100644 --- a/lambda/authorizer/lambda_functions.py +++ b/lambda/authorizer/lambda_functions.py @@ -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__) @@ -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 diff --git a/lambda/configuration/lambda_functions.py b/lambda/configuration/lambda_functions.py index 23ffabdfc..71fe29dfd 100644 --- a/lambda/configuration/lambda_functions.py +++ b/lambda/configuration/lambda_functions.py @@ -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__) @@ -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( @@ -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] @@ -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"}) diff --git a/lambda/management_key.py b/lambda/management_key.py index b1f438cf4..afe6b4053 100644 --- a/lambda/management_key.py +++ b/lambda/management_key.py @@ -17,6 +17,7 @@ import json import logging import os +from datetime import datetime from typing import Any, Dict import boto3 @@ -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]: @@ -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.""" diff --git a/lambda/mcp_server/lambda_functions.py b/lambda/mcp_server/lambda_functions.py index ffa58b4bd..090821520 100644 --- a/lambda/mcp_server/lambda_functions.py +++ b/lambda/mcp_server/lambda_functions.py @@ -17,12 +17,13 @@ import logging import os from decimal import Decimal -from typing import Any, Dict, Optional +from functools import reduce +from typing import Any, Dict, List, Optional import boto3 from boto3.dynamodb.conditions import Attr, Key from utilities.auth import get_username, is_admin -from utilities.common_functions import api_wrapper, get_item, retry_config +from utilities.common_functions import api_wrapper, get_bearer_token, get_groups, get_item, retry_config from .models import McpServerModel, McpServerStatus @@ -33,21 +34,81 @@ table = dynamodb.Table(os.environ["MCP_SERVERS_TABLE_NAME"]) +def replace_bearer_token_header(mcp_server: dict, replacement: str): + """Replace {LISA_BEARER_TOKEN} placeholder with actual bearer token in custom headers.""" + custom_headers = mcp_server.get("customHeaders", {}) + for key, value in custom_headers.items(): + if key.lower() == "authorization" and "{LISA_BEARER_TOKEN}" in value: + custom_headers[key] = value.replace("{LISA_BEARER_TOKEN}", replacement) + + +def _build_groups_condition(groups: List[str]) -> Any: + """Build DynamoDB condition for groups filtering.""" + # Servers with no groups (groups attribute doesn't exist, is null, or is empty array) should be included + no_groups_condition = Attr("groups").not_exists() | Attr("groups").eq(None) | Attr("groups").eq([]) + + # Servers with at least one matching group + group_conditions = [Attr("groups").contains(f"group:{group}") for group in groups] + has_matching_group_condition = reduce(lambda a, b: a | b, group_conditions) + + # Combine: no groups OR has matching group + return no_groups_condition | has_matching_group_condition + + def _get_mcp_servers( user_id: Optional[str] = None, active: Optional[bool] = None, + replace_bearer_token: Optional[str] = None, + groups: Optional[List] = None, ) -> Dict[str, Any]: """Helper function to retrieve mcp servers from DynamoDB.""" filter_expression = None + condition = None # Filter by user_id if provided if user_id: - condition = Attr("owner").eq(user_id) | Attr("owner").eq("lisa:public") + if groups is not None and len(groups) > 0: + # Complex logic when groups are provided: + # 1. User owns server (regardless of groups) OR + # 2. Public server AND (no groups OR has matching groups) OR + # 3. Any server with matching groups (regardless of owner) + + # User owns server (no group restrictions) + user_owns = Attr("owner").eq(user_id) + + # Public server with groups filtering + # Public servers should be included if they have no groups OR matching groups + public_no_groups = Attr("owner").eq("lisa:public") & ( + Attr("groups").not_exists() | Attr("groups").eq(None) | Attr("groups").eq([]) + ) + public_matching_groups = Attr("owner").eq("lisa:public") & reduce( + lambda a, b: a | b, [Attr("groups").contains(f"group:{group}") for group in groups] + ) + public_with_groups_ok = public_no_groups | public_matching_groups + + # Any server with matching groups (regardless of owner) + # Only include servers that actually have groups and match + group_conditions = [Attr("groups").contains(f"group:{group}") for group in groups] + any_matching_groups = reduce(lambda a, b: a | b, group_conditions) + + # Combine: user owns OR (public with groups ok) OR (any matching groups) + condition = user_owns | public_with_groups_ok | any_matching_groups + else: + # Simple logic when no groups: user owns OR public + condition = Attr("owner").eq(user_id) | Attr("owner").eq("lisa:public") + filter_expression = condition if filter_expression is None else filter_expression & condition + + # Filter by active status if provided if active: condition = Attr("status").eq(McpServerStatus.ACTIVE) filter_expression = condition if filter_expression is None else filter_expression & condition + # Filter by user groups if provided (only if not already handled in user_id filter) + if groups is not None and len(groups) > 0 and not user_id: + condition = _build_groups_condition(groups) + filter_expression = condition if filter_expression is None else filter_expression & condition + scan_arguments = { "TableName": os.environ["MCP_SERVERS_TABLE_NAME"], "IndexName": os.environ["MCP_SERVERS_BY_OWNER_INDEX_NAME"], @@ -61,12 +122,19 @@ def _get_mcp_servers( items = [] while True: response = table.scan(**scan_arguments) - items.extend(response.get("Items", [])) + batch_items = response.get("Items", []) + items.extend(batch_items) + if "LastEvaluatedKey" in response: scan_arguments["ExclusiveStartKey"] = response["LastEvaluatedKey"] else: break + # Look through the headers, and replace {LISA_BEARER_TOKEN} with the users + if replace_bearer_token: + for mcp_server in items: + replace_bearer_token_header(mcp_server, replace_bearer_token) + return {"Items": items} @@ -76,6 +144,10 @@ def get(event: dict, context: dict) -> Any: user_id = get_username(event) mcp_server_id = get_mcp_server_id(event) + # Check if showPlaceholder query parameter is present + query_params = event.get("queryStringParameters") or {} + show_placeholder = query_params.get("showPlaceholder") == "1" + # Query for the mcp server response = table.query(KeyConditionExpression=Key("id").eq(mcp_server_id), Limit=1, ScanIndexForward=False) item = get_item(response) @@ -85,25 +157,40 @@ def get(event: dict, context: dict) -> Any: # Check if the user is authorized to get the mcp server is_owner = item["owner"] == user_id or item["owner"] == "lisa:public" - if is_owner or is_admin(event): + groups = item.get("groups", []) + if is_owner or is_admin(event) or _is_member(get_groups(event), groups): # add extra attribute so the frontend doesn't have to determine this if is_owner: item["isOwner"] = True + + # Replace bearer token placeholder unless showPlaceholder is true + if not show_placeholder: + bearer_token = get_bearer_token(event) + if bearer_token: + replace_bearer_token_header(item, bearer_token) + return item raise ValueError(f"Not authorized to get {mcp_server_id}.") +def _is_member(user_groups: List[str], prompt_groups: List[str]) -> bool: + return bool(set(user_groups) & set(prompt_groups)) + + @api_wrapper def list(event: dict, context: dict) -> Dict[str, Any]: """List mcp servers for a user from DynamoDB.""" user_id = get_username(event) + bearer_token = get_bearer_token(event) + if is_admin(event): logger.info(f"Listing all mcp servers for user {user_id} (is_admin)") - return _get_mcp_servers() - logger.info(f"Listing mcp servers for user {user_id}") - return _get_mcp_servers(user_id=user_id, active=True) + return _get_mcp_servers(replace_bearer_token=bearer_token) + + groups = get_groups(event) + return _get_mcp_servers(user_id=user_id, active=True, groups=groups, replace_bearer_token=bearer_token) @api_wrapper @@ -111,7 +198,9 @@ def create(event: dict, context: dict) -> Any: """Create a new mcp server in DynamoDB.""" user_id = get_username(event) body = json.loads(event["body"], parse_float=Decimal) - body["owner"] = user_id if body.get("owner", None) is None else body["owner"] # Set the owner of the mcp server + body["owner"] = ( + user_id if body.get("owner", None) != "lisa:public" else body["owner"] + ) # Set the owner of the mcp server mcp_server_model = McpServerModel(**body) # Insert the new mcp server item into the DynamoDB table @@ -125,6 +214,7 @@ def update(event: dict, context: dict) -> Any: user_id = get_username(event) mcp_server_id = get_mcp_server_id(event) body = json.loads(event["body"], parse_float=Decimal) + body["owner"] = user_id if body.get("owner", None) != "lisa:public" else body["owner"] mcp_server_model = McpServerModel(**body) if mcp_server_id != mcp_server_model.id: diff --git a/lambda/mcp_server/models.py b/lambda/mcp_server/models.py index 06ba470e9..7db48941b 100644 --- a/lambda/mcp_server/models.py +++ b/lambda/mcp_server/models.py @@ -15,7 +15,7 @@ import uuid from datetime import datetime from enum import Enum -from typing import Optional +from typing import List, Optional from pydantic import BaseModel, Field @@ -63,3 +63,6 @@ class McpServerModel(BaseModel): # Status of the server set by admins status: Optional[McpServerStatus] = Field(default=McpServerStatus.INACTIVE) + + # Groups of the MCP server + groups: Optional[List[str]] = Field(default_factory=lambda: None) diff --git a/lambda/mcp_workbench/__init__.py b/lambda/mcp_workbench/__init__.py new file mode 100644 index 000000000..4139ae4d0 --- /dev/null +++ b/lambda/mcp_workbench/__init__.py @@ -0,0 +1,13 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/lambda/mcp_workbench/lambda_functions.py b/lambda/mcp_workbench/lambda_functions.py new file mode 100644 index 000000000..a1603d906 --- /dev/null +++ b/lambda/mcp_workbench/lambda_functions.py @@ -0,0 +1,257 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Lambda functions for managing MCP Tools in AWS S3.""" +import json +import logging +import os +import uuid +from datetime import datetime +from decimal import Decimal +from typing import Any, Dict, Optional + +import boto3 +import botocore.exceptions +from pydantic import BaseModel, Field +from utilities.auth import is_admin +from utilities.common_functions import api_wrapper, retry_config +from utilities.exceptions import HTTPException + +logger = logging.getLogger(__name__) + +# Initialize the S3 resource using environment variables +s3_client = boto3.client("s3", region_name=os.environ["AWS_REGION"], config=retry_config) +WORKBENCH_BUCKET = os.environ.get("WORKBENCH_BUCKET", "") + +MCPWORKBENCH_UUID = str(uuid.uuid5(uuid.NAMESPACE_DNS, "LISA_MCP_WORKBENCH")) + + +class MCPToolModel(BaseModel): + """A Pydantic model representing an MCP Tool.""" + + # The filename/toolId seen by frontend + id: str + + # The Python code content + contents: str + + # Timestamp of when the tool was created/updated + updated_at: Optional[str] = Field(default_factory=lambda: datetime.now().isoformat()) + + @property + def s3_key(self) -> str: + """Get the S3 key for this tool.""" + # Ensure the id ends with .py + if not self.id.endswith(".py"): + return f"{self.id}.py" + return self.id + + +def _get_tool_from_s3(tool_id: str) -> MCPToolModel: + """Helper function to retrieve a tool from S3.""" + # Ensure the tool_id ends with .py + if not tool_id.endswith(".py"): + tool_id = f"{tool_id}.py" + + try: + response = s3_client.get_object(Bucket=WORKBENCH_BUCKET, Key=tool_id) + contents = response["Body"].read().decode("utf-8") + return MCPToolModel( + id=tool_id, + contents=contents, + updated_at=response.get("LastModified", datetime.now()).isoformat(), + ) + except botocore.exceptions.ClientError as e: + code = e.response.get("Error", {}).get("Code") + if code in ("NoSuchKey", "404"): + raise HTTPException(status_code=404, message=f"Tool {tool_id} not found in S3 bucket.") from e + # Log and re-raise as ValueError to keep the function's contract + logger.error("Error retrieving tool from S3: %s", e, exc_info=True) + raise ValueError(f"Failed to retrieve tool: {e}") from e + except Exception as e: + logger.error("Unexpected error retrieving tool from S3: %s", e, exc_info=True) + raise ValueError(f"Failed to retrieve tool: {e}") from e + + +@api_wrapper +def read(event: dict, context: dict) -> Any: + """Retrieve a specific tool from S3.""" + if not is_admin(event): + raise ValueError("Only admin users can access tools.") + + tool_id = event.get("pathParameters", {}).get("toolId") + if not tool_id: + raise ValueError("Missing toolId parameter.") + + logger.info(f"Reading tool with ID: {tool_id}") + + try: + tool = _get_tool_from_s3(tool_id) + return tool.model_dump() + except HTTPException: + # Let HTTPException pass through for proper status codes + raise + except ValueError as e: + # This is likely from _get_tool_from_s3 already properly formatted + raise e + except Exception as e: + logger.error("Unexpected error reading tool: %s", e, exc_info=True) + raise ValueError(f"Failed to read tool: {e}") from e + + +@api_wrapper +def list(event: dict, context: dict) -> Dict[str, Any]: + """List all tools from S3.""" + if not is_admin(event): + raise ValueError("Only admin users can access tools.") + + try: + response = s3_client.list_objects_v2(Bucket=WORKBENCH_BUCKET, Prefix="") + + tools = [] + for obj in response.get("Contents", []): + key = obj["Key"] + # Only include Python files + if key.endswith(".py"): + # We exclude the actual contents for the list operation to save bandwidth + tools.append( + { + "id": key, + "updated_at": obj.get("LastModified", datetime.now()).isoformat(), + "size": obj.get("Size", 0), + } + ) + + return {"tools": tools} + except botocore.exceptions.ClientError as e: + logger.error("Error listing tools from S3: %s", e, exc_info=True) + raise ValueError(f"Failed to list tools: {e}") from e + except Exception as e: + logger.error("Unexpected error listing tools: %s", e, exc_info=True) + raise ValueError(f"Failed to list tools: {e}") from e + + +@api_wrapper +def create(event: dict, context: dict) -> Any: + """Create a new tool in S3.""" + if not is_admin(event): + raise ValueError("Only admin users can access tools.") + + try: + body = json.loads(event["body"], parse_float=Decimal) + + # Ensure the required fields are present + if "id" not in body or "contents" not in body: + raise ValueError("Missing required fields: 'id' and 'contents' are required.") + + # Create the tool model + tool_model = MCPToolModel(id=body["id"], contents=body["contents"]) + + # Upload to S3 + s3_client.put_object( + Bucket=WORKBENCH_BUCKET, + Key=tool_model.s3_key, + Body=tool_model.contents.encode("utf-8"), + ContentType="text/x-python", + ) + + return tool_model.model_dump() + except botocore.exceptions.ClientError as e: + logger.error("Error creating tool in S3: %s", e, exc_info=True) + raise ValueError(f"Failed to create tool: {e}") from e + except json.JSONDecodeError as e: + logger.error("Invalid JSON in request body: %s", e, exc_info=True) + raise ValueError(f"Invalid request body: {e}") from e + except Exception as e: + logger.error("Unexpected error creating tool: %s", e, exc_info=True) + raise ValueError(f"Failed to create tool: {e}") from e + + +@api_wrapper +def update(event: dict, context: dict) -> Any: + """Update an existing tool in S3.""" + if not is_admin(event): + raise ValueError("Only admin users can access tools.") + + try: + tool_id = event.get("pathParameters", {}).get("toolId") + if not tool_id: + raise ValueError("Missing toolId parameter.") + + body = json.loads(event["body"], parse_float=Decimal) + + # Ensure the contents field is present + if "contents" not in body: + raise ValueError("Missing required field: 'contents' is required.") + + # Check if the tool exists + try: + _get_tool_from_s3(tool_id) + except HTTPException: + raise HTTPException(status_code=404, message=f"Tool {tool_id} does not exist.") + + # Create updated tool model + tool_model = MCPToolModel(id=tool_id, contents=body["contents"]) + + # Update in S3 + s3_client.put_object( + Bucket=WORKBENCH_BUCKET, + Key=tool_model.s3_key, + Body=tool_model.contents.encode("utf-8"), + ContentType="text/x-python", + ) + + return tool_model.model_dump() + except botocore.exceptions.ClientError as e: + logger.error("Error updating tool in S3: %s", e, exc_info=True) + raise ValueError(f"Failed to update tool: {e}") from e + except json.JSONDecodeError as e: + logger.error("Invalid JSON in request body: %s", e, exc_info=True) + raise ValueError(f"Invalid request body: {e}") from e + except Exception as e: + logger.error("Unexpected error updating tool: %s", e, exc_info=True) + raise ValueError(f"Failed to update tool: {e}") from e + + +@api_wrapper +def delete(event: dict, context: dict) -> Dict[str, str]: + """Delete a tool from S3.""" + if not is_admin(event): + raise ValueError("Only admin users can access tools.") + + try: + tool_id = event.get("pathParameters", {}).get("toolId") + if not tool_id: + raise ValueError("Missing toolId parameter.") + + # Ensure the tool_id ends with .py + if not tool_id.endswith(".py"): + tool_id = f"{tool_id}.py" + + # Check if the tool exists before deletion + try: + _get_tool_from_s3(tool_id) + except HTTPException: + raise HTTPException(status_code=404, message=f"Tool {tool_id} does not exist.") + + # Delete from S3 + s3_client.delete_object(Bucket=WORKBENCH_BUCKET, Key=tool_id) + + return {"status": "ok", "message": f"Tool {tool_id} deleted successfully."} + except botocore.exceptions.ClientError as e: + logger.error("Error deleting tool from S3: %s", e, exc_info=True) + raise ValueError(f"Failed to delete tool: {e}") from e + except Exception as e: + logger.error("Unexpected error deleting tool: %s", e, exc_info=True) + raise ValueError(f"Failed to delete tool: {e}") from e diff --git a/lambda/mcp_workbench/s3_event_handler.py b/lambda/mcp_workbench/s3_event_handler.py new file mode 100644 index 000000000..3be8a5dc6 --- /dev/null +++ b/lambda/mcp_workbench/s3_event_handler.py @@ -0,0 +1,202 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Lambda to handle S3 events and trigger MCP Workbench service redeployment.""" + +import json +import logging +import os +from typing import Any, Dict + +import boto3 +from botocore.exceptions import ClientError +from utilities.common_functions import retry_config + +# Configure logging +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +# Initialize clients +ecs_client = boto3.client("ecs", region_name=os.environ["AWS_REGION"], config=retry_config) +ssm_client = boto3.client("ssm", region_name=os.environ["AWS_REGION"], config=retry_config) + + +def handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]: + """ + Handle S3 events from EventBridge and trigger MCP Workbench service redeployment. + + This function is triggered by EventBridge when S3 objects are created or deleted + in the MCP Workbench bucket. It forces a new deployment of the MCPWORKBENCH ECS service. + """ + logger.info(f"Received S3 event: {json.dumps(event, default=str)}") + + try: + # Extract event details + detail = event.get("detail", {}) + bucket_name = detail.get("bucket", {}).get("name") + event_name = detail.get("eventName", "") + + if not bucket_name: + logger.error("Missing bucket name in event details") + return {"statusCode": 400, "body": json.dumps("Missing bucket name")} + + logger.info(f"Processing S3 event '{event_name}' for bucket: {bucket_name}") + + # Get ECS cluster and service names + cluster_name = get_cluster_name() + service_name = get_service_name() + + # Force new deployment of the MCPWORKBENCH service + deployment_response = force_service_deployment(cluster_name, service_name) + + deployment_id = deployment_response.get("service", {}).get("deployments", [{}])[0].get("id", "unknown") + logger.info(f"Service deployment triggered successfully. Deployment ARN: {deployment_id}") + + return { + "statusCode": 200, + "body": json.dumps( + { + "message": "MCPWORKBENCH service redeployment triggered successfully", + "bucket": bucket_name, + "event": event_name, + "cluster": cluster_name, + "service": service_name, + "deployment_id": deployment_response.get("service", {}) + .get("deployments", [{}])[0] + .get("id", "unknown"), + } + ), + } + + except Exception as e: + logger.error(f"Error processing S3 event: {str(e)}") + return {"statusCode": 500, "body": json.dumps(f"Error: {str(e)}")} + + +def get_cluster_name() -> str: + """ + Get the ECS cluster name from environment variables or construct it from deployment info. + """ + try: + # First try environment variable + cluster_name = os.environ.get("ECS_CLUSTER_NAME") + if cluster_name: + return cluster_name + + # Fall back to constructing from deployment info + deployment_prefix = os.environ.get("DEPLOYMENT_PREFIX") + if not deployment_prefix: + raise ValueError("DEPLOYMENT_PREFIX environment variable not set") + + # Get deployment name from SSM parameter + deployment_name_param = f"{deployment_prefix}/deploymentName" + try: + response = ssm_client.get_parameter(Name=deployment_name_param) + deployment_name = response["Parameter"]["Value"] + except ClientError: + # If parameter doesn't exist, extract from deployment prefix + # deployment_prefix format is typically /deploymentName-stage + deployment_name = deployment_prefix.split("/")[-1].split("-")[0] + + # Construct cluster name using the same pattern as CDK: createCdkId([config.deploymentName, identifier], 32, 2) + # The identifier for the API cluster is typically "serve" or similar + api_name = os.environ.get("API_NAME", "serve") + cluster_name = f"{deployment_name}-{api_name}" + + logger.info(f"Constructed cluster name: {cluster_name}") + return cluster_name + + except Exception as e: + logger.error(f"Error getting cluster name: {e}") + raise + + +def get_service_name() -> str: + """ + Get the MCPWORKBENCH service name from environment variables or construct it. + """ + try: + # First try environment variable + service_name = os.environ.get("MCPWORKBENCH_SERVICE_NAME") + if service_name: + return service_name + + # Fall back to constructing the service name + # Based on CDK code: createCdkId([name], 32, 2) where name is ECSTasks.MCPWORKBENCH + service_name = "MCPWORKBENCH" + + logger.info(f"Using service name: {service_name}") + return service_name + + except Exception as e: + logger.error(f"Error getting service name: {e}") + raise + + +def force_service_deployment(cluster_name: str, service_name: str) -> Dict[str, Any]: + """ + Force a new deployment of the specified ECS service. + """ + try: + logger.info(f"Forcing new deployment for service '{service_name}' in cluster '{cluster_name}'") + + # Call ECS update_service with forceNewDeployment=True + response = ecs_client.update_service(cluster=cluster_name, service=service_name, forceNewDeployment=True) + + logger.info(f"Successfully triggered new deployment for service '{service_name}'") + return response + + except ClientError as e: + error_code = e.response.get("Error", {}).get("Code", "Unknown") + error_message = e.response.get("Error", {}).get("Message", str(e)) + + if error_code == "ServiceNotFoundException": + logger.error(f"Service '{service_name}' not found in cluster '{cluster_name}'") + elif error_code == "ClusterNotFoundException": + logger.error(f"Cluster '{cluster_name}' not found") + else: + logger.error(f"ECS error ({error_code}): {error_message}") + + raise + except Exception as e: + logger.error(f"Unexpected error forcing service deployment: {e}") + raise + + +def validate_s3_event(event: Dict[str, Any]) -> bool: + """ + Validate that the event is a proper S3 event from EventBridge. + """ + try: + source = event.get("source") + detail_type = event.get("detail-type") + detail = event.get("detail", {}) + + # Check if it's an S3 event + if source not in ["aws.s3", "debug"]: + return False + + # Check if it's an object event + if detail_type not in ["Object Created", "Object Deleted"]: + return False + + # Check if bucket information is present + if not detail.get("bucket", {}).get("name"): + return False + + return True + + except Exception as e: + logger.error(f"Error validating S3 event: {e}") + return False diff --git a/lambda/models/domain_objects.py b/lambda/models/domain_objects.py index 3f47912c7..bf0ccd79b 100644 --- a/lambda/models/domain_objects.py +++ b/lambda/models/domain_objects.py @@ -252,6 +252,7 @@ class CreateModelRequest(BaseModel): streaming: Optional[bool] = False features: Optional[List[ModelFeature]] = None allowedGroups: Optional[List[str]] = None + apiKey: Optional[str] = None @model_validator(mode="after") def validate_create_model_request(self) -> Self: diff --git a/lambda/models/state_machine/create_model.py b/lambda/models/state_machine/create_model.py index 07e6d0a82..769acbaa7 100644 --- a/lambda/models/state_machine/create_model.py +++ b/lambda/models/state_machine/create_model.py @@ -351,7 +351,9 @@ def handle_add_model_to_litellm(event: Dict[str, Any], context: Any) -> Dict[str # Fallback to default if JSON parsing fails litellm_params = {} - litellm_params["api_key"] = "ignored" # pragma: allowlist-secret not a real key, but needed for LiteLLM to be happy + litellm_params["api_key"] = event.get( + "apiKey", "ignored" + ) # pragma: allowlist-secret not a real key, but needed for LiteLLM to be happy litellm_params["drop_params"] = True # drop unrecognized param instead of failing the request on it if is_lisa_managed: diff --git a/lambda/utilities/common_functions.py b/lambda/utilities/common_functions.py index 86651850a..def669737 100644 --- a/lambda/utilities/common_functions.py +++ b/lambda/utilities/common_functions.py @@ -22,7 +22,7 @@ from contextvars import ContextVar from decimal import Decimal from functools import cache -from typing import Any, Callable, cast, Dict, List, TypeVar, Union +from typing import Any, Callable, cast, Dict, List, Optional, TypeVar, Union import boto3 from botocore.config import Config @@ -269,6 +269,11 @@ def generate_exception_response( logger.exception(e) elif hasattr(e, "http_status_code"): status_code = e.http_status_code + e = e.message # type: ignore [assignment] + logger.exception(e) + elif hasattr(e, "status_code"): + status_code = e.status_code + e = e.message # type: ignore [assignment] logger.exception(e) else: error_msg = str(e) @@ -478,3 +483,39 @@ def user_has_group_access(user_groups: List[str], allowed_groups: List[str]) -> # Check if user has at least one matching group return len(set(user_groups).intersection(set(allowed_groups))) > 0 + + +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 get_bearer_token(event, with_prefix: bool = True): + """ + Extracts a Bearer token from the Authorization header in a Lambda event. + + Args: + event (dict): AWS Lambda event (API Gateway / ALB proxy style). + + Returns: + str | None: The token string if present and properly formatted, else None. + """ + headers = event.get("headers") or {} + # Headers may vary in casing + auth_header = headers.get("Authorization") or headers.get("authorization") + if not auth_header: + return None + + if not auth_header.lower().startswith("bearer "): + return None + + # Return the token after "Bearer " + return auth_header.split(" ", 1)[1].strip() diff --git a/lib/api-base/ecsCluster.ts b/lib/api-base/ecsCluster.ts index 1dfc4b1e9..5df4569c4 100644 --- a/lib/api-base/ecsCluster.ts +++ b/lib/api-base/ecsCluster.ts @@ -16,12 +16,13 @@ // ECS Cluster Construct. import { Duration, RemovalPolicy } from 'aws-cdk-lib'; -import { BlockDeviceVolume, GroupMetrics, Monitoring } from 'aws-cdk-lib/aws-autoscaling'; +import { AdjustmentType, AutoScalingGroup, BlockDeviceVolume, GroupMetrics, Monitoring, UpdatePolicy } from 'aws-cdk-lib/aws-autoscaling'; import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; -import { Metric, Stats } from 'aws-cdk-lib/aws-cloudwatch'; -import { InstanceType, ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; +import { Metric } from 'aws-cdk-lib/aws-cloudwatch'; +import { InstanceType, ISecurityGroup, Port, SecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { AmiHardwareType, + AsgCapacityProvider, Cluster, ContainerDefinition, ContainerInsights, @@ -39,7 +40,9 @@ import { } from 'aws-cdk-lib/aws-ecs'; import { ApplicationLoadBalancer, + ApplicationTargetGroup, BaseApplicationListenerProps, + ListenerCondition, SslPolicy, } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; import { Effect, IRole, ManagedPolicy, PolicyStatement, Role } from 'aws-cdk-lib/aws-iam'; @@ -47,10 +50,15 @@ import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { Construct } from 'constructs'; import { createCdkId } from '../core/utils'; -import { BaseProps, Ec2Metadata, ECSConfig } from '../schema'; +import { BaseProps, Config, Ec2Metadata, ECSConfig, TaskDefinition } from '../schema'; import { Vpc } from '../networking/vpc'; import { CodeFactory } from '../util'; +export enum ECSTasks { + REST = 'REST', + MCPWORKBENCH = 'MCPWORKBENCH', +} + /** * Properties for the ECSCluster Construct. * @@ -59,6 +67,7 @@ import { CodeFactory } from '../util'; * @property {Vpc} vpc - The virtual private cloud (VPC). */ type ECSClusterProps = { + identifier: string, ecsConfig: ECSConfig; securityGroup: ISecurityGroup; vpc: Vpc; @@ -68,15 +77,113 @@ type ECSClusterProps = { * Create an ECS model. */ export class ECSCluster extends Construct { - /** ECS Cluster container definition */ - public readonly container: ContainerDefinition; - - /** IAM role associated with the ECS Cluster task */ - public readonly taskRole: IRole; /** Endpoint URL of application load balancer for the cluster. */ public readonly endpointUrl: string; + /** Map of all container definitions by identifier */ + public readonly containers: Partial> = {}; + + /** Map of all task roles by identifier */ + public readonly taskRoles: Partial> = {}; + + /** Map of all services by identifier */ + public readonly services: Partial> = {}; + + private readonly targetGroups: Partial> = {}; + + /** + * Creates a task definition with its associated container and IAM role (base method). + * + * @param identifier - The identifier for the task (e.g., 'fastapi', 'workbenchHosting') + * @param config - The base configuration + * @param taskDefinition - The ECS configuration + * @param volumes - Array of volumes to mount + * @param mountPoints - Array of mount points for the container + * @param logGroup - CloudWatch log group for container logs + * @param buildArgs - Optional build arguments for the container image + * @returns Object containing task definition, container, and task role + */ + private createTaskDefinition ( + taskDefinitionName: string, + config: Config, + taskDefinition: TaskDefinition, + baseEnvironment: Record, + ecsConfig: ECSConfig, + volumes: Volume[], + mountPoints: MountPoint[], + logGroup: LogGroup, + // hostPort: number, + taskRole: IRole, + executionRole?: IRole + ): { taskDefinition: Ec2TaskDefinition, container: ContainerDefinition } { + const ec2TaskDefinition = new Ec2TaskDefinition(this, createCdkId([taskDefinitionName, 'Ec2TaskDefinition']), { + family: createCdkId([config.deploymentName, taskDefinitionName], 32, 2), + volumes, + ...(taskRole && { taskRole }), + ...(executionRole && { executionRole }), + }); + + // Grant CloudWatch logs permissions to both task role and execution role + logGroup.grantWrite(taskRole); + if (executionRole) { + logGroup.grantWrite(executionRole); + } else { + // If no custom execution role, ensure the default execution role has CloudWatch permissions + // This is critical for log stream creation during container startup + ec2TaskDefinition.addToExecutionRolePolicy(new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'logs:CreateLogGroup', + 'logs:CreateLogStream', + 'logs:PutLogEvents', + 'logs:DescribeLogStreams' + ], + resources: [logGroup.logGroupArn, `${logGroup.logGroupArn}:*`] + })); + } + + // Add container to task definition + const containerHealthCheckConfig = taskDefinition.containerConfig.healthCheckConfig; + const containerHealthCheck: HealthCheck = { + command: containerHealthCheckConfig.command, + interval: Duration.seconds(containerHealthCheckConfig.interval), + startPeriod: Duration.seconds(containerHealthCheckConfig.startPeriod), + timeout: Duration.seconds(containerHealthCheckConfig.timeout), + retries: containerHealthCheckConfig.retries, + }; + + const linuxParameters = + taskDefinition.containerConfig.sharedMemorySize > 0 + ? new LinuxParameters(this, createCdkId([taskDefinitionName, 'LinuxParameters']), { + sharedMemorySize: taskDefinition.containerConfig.sharedMemorySize, + }) + : undefined; + + const image = CodeFactory.createImage(taskDefinition.containerConfig.image, this, taskDefinitionName, ecsConfig.buildArgs); + + const container = ec2TaskDefinition.addContainer(createCdkId([taskDefinitionName, 'Container']), { + containerName: createCdkId([config.deploymentName, taskDefinitionName], 32, 2), + image, + environment: {...baseEnvironment, ...taskDefinition.environment}, + logging: LogDriver.awsLogs({ + logGroup: logGroup, + streamPrefix: taskDefinitionName + }), + gpuCount: Ec2Metadata.get(ecsConfig.instanceType).gpuCount, + memoryReservationMiB: taskDefinition.containerMemoryReservationMiB, + memoryLimitMiB: ecsConfig.containerMemoryBuffer, + portMappings: [{ hostPort: 0, containerPort: taskDefinition.applicationTarget?.port ?? 8080, protocol: Protocol.TCP }], + healthCheck: containerHealthCheck, + // Model containers need to run with privileged set to true + privileged: taskDefinition.containerConfig.privileged ?? ecsConfig.amiHardwareType === AmiHardwareType.GPU, + ...(linuxParameters && { linuxParameters }), + }); + container.addMountPoints(...mountPoints); + + return { taskDefinition: ec2TaskDefinition, container }; + } + /** * @param {Construct} scope - The parent or owner of the construct. * @param {string} id - The unique identifier for the construct within its scope. @@ -84,17 +191,37 @@ export class ECSCluster extends Construct { */ constructor (scope: Construct, id: string, props: ECSClusterProps) { super(scope, id); - const { config, vpc, securityGroup, ecsConfig } = props; + const { config, identifier, vpc, securityGroup, ecsConfig } = props; + + // Retrieve execution role if it has been overridden + const executionRole = config.roles ? Role.fromRoleArn( + this, + createCdkId([identifier, 'ER']), + StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/roles/${identifier}EX`), + ) : undefined; + + // Create ECS task definition + const taskRole = Role.fromRoleArn( + this, + createCdkId([identifier, 'TR']), + StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/roles/${identifier}`), + ); // Create ECS cluster - const cluster = new Cluster(this, createCdkId(['Cl']), { - clusterName: createCdkId([config.deploymentName, ecsConfig.identifier], 32, 2), + const cluster = new Cluster(this, createCdkId([config.deploymentName, config.deploymentStage, 'Cl']), { + clusterName: createCdkId([config.deploymentName, config.deploymentStage, identifier], 32, 2), vpc: vpc.vpc, containerInsightsV2: !config.region.includes('iso') ? ContainerInsights.ENABLED : ContainerInsights.DISABLED, }); + const asgSecurityGroup = new SecurityGroup(this, 'RestAsgSecurityGroup', { + allowAllOutbound: true, + vpc: vpc.vpc, + }); + // Create auto-scaling group - const autoScalingGroup = cluster.addCapacity(createCdkId(['ASG']), { + const autoScalingGroup = new AutoScalingGroup(this, createCdkId([config.deploymentName, config.deploymentStage, 'ASG']), { + vpc: vpc.vpc, vpcSubnets: vpc.subnetSelection, instanceType: new InstanceType(ecsConfig.instanceType), machineImage: EcsOptimizedImage.amazonLinux2(ecsConfig.amiHardwareType), @@ -103,7 +230,6 @@ export class ECSCluster extends Construct { cooldown: Duration.seconds(ecsConfig.autoScalingConfig.cooldown), groupMetrics: [GroupMetrics.all()], instanceMonitoring: Monitoring.DETAILED, - newInstancesProtectedFromScaleIn: false, defaultInstanceWarmup: Duration.seconds(ecsConfig.autoScalingConfig.defaultInstanceWarmup), blockDevices: [ { @@ -113,9 +239,62 @@ export class ECSCluster extends Construct { }), }, ], + securityGroup: asgSecurityGroup, + autoScalingGroupName: createCdkId([config.deploymentName, config.deploymentStage, identifier], 32, 2), + updatePolicy: UpdatePolicy.rollingUpdate({}) + }); + + const asgCapacityProvider = new AsgCapacityProvider(this, createCdkId([config.deploymentName, config.deploymentStage, 'AsgCapacityProvider']), { + autoScalingGroup, + // Managed scaling tracks cluster reservation to add/remove instances automatically + // when services want more tasks than the cluster can fit. + // targetCapacityPercent ~ how "full" you want the cluster (by CPU/memory reservation). + // 90 means try to keep instances ~90% reserved before adding more. + // capacityProviderName: [config.deploymentName, config.deploymentStage, 'cp-ec2'].join('-'), + + // disable managed scaling because we are going to setup rules to do it + enableManagedScaling: false, + enableManagedTerminationProtection: false, + }); + cluster.addAsgCapacityProvider(asgCapacityProvider); + + const reservationMetric = new Metric({ + namespace: 'AWS/ECS/CapacityProvider', + metricName: 'CapacityProviderReservation', + // The dimensions are crucial to target the specific cluster and capacity provider + dimensionsMap: { + ClusterName: cluster.clusterName, + CapacityProviderName: asgCapacityProvider.capacityProviderName, + }, + statistic: 'Average', + period: Duration.minutes(1), + }); + + autoScalingGroup.scaleOnMetric(createCdkId(['ASG', identifier, 'ScaleIn']), { + metric: reservationMetric, + scalingSteps: [ + { lower: 60, change: 1 }, + { lower: 40, change: 2 } + ], + evaluationPeriods: 5, + adjustmentType: AdjustmentType.CHANGE_IN_CAPACITY, + cooldown: Duration.seconds(300) + }); + + autoScalingGroup.scaleOnMetric(createCdkId(['ASG', identifier, 'ScaleOut']), { + metric: reservationMetric, + scalingSteps: [ + { upper: 80, change: 1 }, + { upper: 90, change: 2 } + ], + evaluationPeriods: 2, + adjustmentType: AdjustmentType.CHANGE_IN_CAPACITY, + cooldown: Duration.seconds(120) }); - const environment = ecsConfig.environment; + const baseEnvironment: { + [key: string]: string; + } = {}; const volumes: Volume[] = []; const mountPoints: MountPoint[] = []; @@ -180,113 +359,25 @@ export class ECSCluster extends Construct { volumes.push(pkiVolume); mountPoints.push(pkiMountPoint); // Requires mount point /etc/pki from host - environment.SSL_CERT_DIR = '/etc/pki/tls/certs'; - environment.SSL_CERT_FILE = config.certificateAuthorityBundle; - environment.REQUESTS_CA_BUNDLE = config.certificateAuthorityBundle; - environment.AWS_CA_BUNDLE = config.certificateAuthorityBundle; - environment.CURL_CA_BUNDLE = config.certificateAuthorityBundle; + baseEnvironment.SSL_CERT_DIR = '/etc/pki/tls/certs'; + baseEnvironment.SSL_CERT_FILE = config.certificateAuthorityBundle; + baseEnvironment.REQUESTS_CA_BUNDLE = config.certificateAuthorityBundle; + baseEnvironment.AWS_CA_BUNDLE = config.certificateAuthorityBundle; + baseEnvironment.CURL_CA_BUNDLE = config.certificateAuthorityBundle; } // Create CloudWatch log group with explicit retention - const logGroup = new LogGroup(this, createCdkId([ecsConfig.identifier, 'LogGroup']), { - logGroupName: `/aws/ecs/${config.deploymentName}-${ecsConfig.identifier}`, + const logGroup = new LogGroup(this, createCdkId([identifier, 'LogGroup']), { + logGroupName: `/aws/ecs/${config.deploymentName}-${identifier}`, retention: RetentionDays.ONE_WEEK, removalPolicy: config.removalPolicy }); - // Retrieve execution role if it has been overridden - const executionRole = config.roles ? Role.fromRoleArn( - this, - createCdkId([ecsConfig.identifier, 'ER']), - StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/roles/${ecsConfig.identifier}EX`), - ) : undefined; - - // Create ECS task definition - const taskRole = Role.fromRoleArn( - this, - createCdkId([ecsConfig.identifier, 'TR']), - StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/roles/${ecsConfig.identifier}`), - ); - const taskDefinition = new Ec2TaskDefinition(this, createCdkId([ecsConfig.identifier, 'Ec2TaskDefinition']), { - family: createCdkId([config.deploymentName, ecsConfig.identifier], 32, 2), - volumes, - ...(taskRole && { taskRole }), - ...(executionRole && { executionRole }), - }); - - // Grant CloudWatch logs permissions to both task role and execution role - logGroup.grantWrite(taskRole); - if (executionRole) { - logGroup.grantWrite(executionRole); - } else { - // If no custom execution role, ensure the default execution role has CloudWatch permissions - // This is critical for log stream creation during container startup - taskDefinition.addToExecutionRolePolicy(new PolicyStatement({ - effect: Effect.ALLOW, - actions: [ - 'logs:CreateLogGroup', - 'logs:CreateLogStream', - 'logs:PutLogEvents', - 'logs:DescribeLogStreams' - ], - resources: [logGroup.logGroupArn, `${logGroup.logGroupArn}:*`] - })); - } - // Add container to task definition - const containerHealthCheckConfig = ecsConfig.containerConfig.healthCheckConfig; - const containerHealthCheck: HealthCheck = { - command: containerHealthCheckConfig.command, - interval: Duration.seconds(containerHealthCheckConfig.interval), - startPeriod: Duration.seconds(containerHealthCheckConfig.startPeriod), - timeout: Duration.seconds(containerHealthCheckConfig.timeout), - retries: containerHealthCheckConfig.retries, - }; - - const linuxParameters = - ecsConfig.containerConfig.sharedMemorySize > 0 - ? new LinuxParameters(this, createCdkId([ecsConfig.identifier, 'LinuxParameters']), { - sharedMemorySize: ecsConfig.containerConfig.sharedMemorySize, - }) - : undefined; - - const image = CodeFactory.createImage(ecsConfig.containerConfig.image, this, ecsConfig.identifier, ecsConfig.buildArgs); - - const container = taskDefinition.addContainer(createCdkId([ecsConfig.identifier, 'Container']), { - containerName: createCdkId([config.deploymentName, ecsConfig.identifier], 32, 2), - image, - environment, - logging: LogDriver.awsLogs({ - logGroup: logGroup, - streamPrefix: ecsConfig.identifier - }), - gpuCount: Ec2Metadata.get(ecsConfig.instanceType).gpuCount, - memoryReservationMiB: Ec2Metadata.get(ecsConfig.instanceType).memory - ecsConfig.containerMemoryBuffer, - portMappings: [{ hostPort: 80, containerPort: 8080, protocol: Protocol.TCP }], - healthCheck: containerHealthCheck, - // Model containers need to run with privileged set to true - privileged: ecsConfig.amiHardwareType === AmiHardwareType.GPU, - ...(linuxParameters && { linuxParameters }), - }); - container.addMountPoints(...mountPoints); - - // Create ECS service - const serviceProps: Ec2ServiceProps = { - cluster: cluster, - daemon: true, - serviceName: createCdkId([config.deploymentName, ecsConfig.identifier], 32, 2), - taskDefinition: taskDefinition, - circuitBreaker: !config.region.includes('iso') ? { rollback: true } : undefined, - }; - - const service = new Ec2Service(this, createCdkId([ecsConfig.identifier, 'Ec2Svc']), serviceProps); - - service.node.addDependency(autoScalingGroup); - // Create application load balancer - const loadBalancer = new ApplicationLoadBalancer(this, createCdkId([ecsConfig.identifier, 'ALB']), { + const loadBalancer = new ApplicationLoadBalancer(this, createCdkId([config.deploymentName, config.deploymentStage, identifier, 'ALB']), { deletionProtection: config.removalPolicy !== RemovalPolicy.DESTROY, internetFacing: ecsConfig.internetFacing, - loadBalancerName: createCdkId([config.deploymentName, ecsConfig.identifier], 32, 2).toLowerCase(), + loadBalancerName: createCdkId([config.deploymentName, config.deploymentStage, identifier], 32, 2).toLowerCase(), dropInvalidHeaderFields: true, securityGroup, vpc: vpc.vpc, @@ -294,6 +385,8 @@ export class ECSCluster extends Construct { idleTimeout: Duration.seconds(600) }); + asgSecurityGroup.addIngressRule(securityGroup, Port.allTcp()); + // Add listener const listenerProps: BaseApplicationListenerProps = { port: ecsConfig.loadBalancerConfig.sslCertIamArn ? 443 : 80, @@ -305,54 +398,94 @@ export class ECSCluster extends Construct { }; const listener = loadBalancer.addListener( - createCdkId([ecsConfig.identifier, 'ApplicationListener']), + createCdkId([identifier, 'ApplicationListener']), listenerProps, ); const protocol = listenerProps.port === 443 ? 'https' : 'http'; - // Add targets - const loadBalancerHealthCheckConfig = ecsConfig.loadBalancerConfig.healthCheckConfig; - const targetGroup = listener.addTargets(createCdkId([ecsConfig.identifier, 'TgtGrp']), { - targetGroupName: createCdkId([config.deploymentName, ecsConfig.identifier], 32, 2).toLowerCase(), - healthCheck: { - path: loadBalancerHealthCheckConfig.path, - interval: Duration.seconds(loadBalancerHealthCheckConfig.interval), - timeout: Duration.seconds(loadBalancerHealthCheckConfig.timeout), - healthyThresholdCount: loadBalancerHealthCheckConfig.healthyThresholdCount, - unhealthyThresholdCount: loadBalancerHealthCheckConfig.unhealthyThresholdCount, - }, - port: 80, - targets: [service], - }); - - // ALB metric for ASG to use for auto scaling EC2 instances - // TODO: Update this to step scaling for embedding models?? - const requestCountPerTargetMetric = new Metric({ - metricName: ecsConfig.autoScalingConfig.metricConfig.albMetricName, - namespace: 'AWS/ApplicationELB', - dimensionsMap: { - TargetGroup: targetGroup.targetGroupFullName, - LoadBalancer: loadBalancer.loadBalancerFullName, - }, - statistic: Stats.SAMPLE_COUNT, - period: Duration.seconds(ecsConfig.autoScalingConfig.metricConfig.duration), - }); - - // Create hook to scale on ALB metric count exceeding thresholds - autoScalingGroup.scaleToTrackMetric(createCdkId([ecsConfig.identifier, 'ScalingPolicy']), { - metric: requestCountPerTargetMetric, - targetValue: ecsConfig.autoScalingConfig.metricConfig.targetValue, - estimatedInstanceWarmup: Duration.seconds(ecsConfig.autoScalingConfig.metricConfig.duration), - }); - const domain = ecsConfig.loadBalancerConfig.domainName !== null ? ecsConfig.loadBalancerConfig.domainName : loadBalancer.loadBalancerDnsName; this.endpointUrl = `${protocol}://${domain}`; + baseEnvironment.CORS_ORIGINS = [loadBalancer.loadBalancerDnsName, ecsConfig.loadBalancerConfig.domainName].filter(Boolean) + .map((domain) => `${protocol}://${domain}`) + .concat('*') + .join(','); + + Object.entries(ecsConfig.tasks).forEach(([name, definition]) => { + const taskResult = this.createTaskDefinition( + name, + config, + definition, + baseEnvironment, + ecsConfig, + volumes, + mountPoints, + logGroup, + taskRole, + executionRole + ); + const { taskDefinition, container } = taskResult; + + // Create ECS service for primary task + const serviceProps: Ec2ServiceProps = { + cluster: cluster, + serviceName: createCdkId([name], 32, 2), + taskDefinition: taskDefinition, + circuitBreaker: !config.region.includes('iso') ? { rollback: true } : undefined, + capacityProviderStrategies: [ + { capacityProvider: asgCapacityProvider.capacityProviderName, weight: 1 } + ] + }; - // Update - this.container = container; - this.taskRole = taskRole; + const service = new Ec2Service(this, createCdkId([config.deploymentName, name, 'Ec2Svc']), serviceProps); + const scalableTaskCount = service.autoScaleTaskCount({ + minCapacity: 1, + // 10 is just a magic number we don't expect to hit because we don't have better data on this + maxCapacity: 10 + }); + service.node.addDependency(autoScalingGroup); + + // since our containers are using ephemeral ports, the load balancer must be allowed to access them + service.connections.allowFrom(loadBalancer, Port.allTcp()); + + // Create target groups for both services + const loadBalancerHealthCheckConfig = ecsConfig.loadBalancerConfig.healthCheckConfig; + + const targetGroup = listener.addTargets(createCdkId([identifier, name, 'TgtGrp']), { + targetGroupName: createCdkId([config.deploymentName, identifier, name], 32, 2).toLowerCase(), + healthCheck: { + path: loadBalancerHealthCheckConfig.path, + interval: Duration.seconds(loadBalancerHealthCheckConfig.interval), + timeout: Duration.seconds(loadBalancerHealthCheckConfig.timeout), + healthyThresholdCount: loadBalancerHealthCheckConfig.healthyThresholdCount, + unhealthyThresholdCount: loadBalancerHealthCheckConfig.unhealthyThresholdCount, + }, + port: 80, + targets: [service], + priority: definition.applicationTarget?.priority, + conditions: definition.applicationTarget?.conditions?.map(({ type, values }) => { + switch (type) { + case 'pathPatterns': + return ListenerCondition.pathPatterns(values); + } + }) + }); + + scalableTaskCount.scaleOnRequestCount(createCdkId([identifier, 'ScalingPolicy']), { + requestsPerTarget: ecsConfig.autoScalingConfig.metricConfig.targetValue, + targetGroup, + scaleInCooldown: Duration.seconds(ecsConfig.autoScalingConfig.metricConfig.duration), + scaleOutCooldown: Duration.seconds(ecsConfig.autoScalingConfig.metricConfig.duration) + }); + + // Store in maps for future reference + const ecsTasksKey = name as keyof typeof ECSTasks; + this.containers[ecsTasksKey] = container; + this.taskRoles[ecsTasksKey] = taskRole; + this.services[ecsTasksKey] = service; + this.targetGroups[ecsTasksKey] = targetGroup; + }); } } diff --git a/lib/api-base/fastApiContainer.ts b/lib/api-base/fastApiContainer.ts index 890a0f8c7..6bdc6276b 100644 --- a/lib/api-base/fastApiContainer.ts +++ b/lib/api-base/fastApiContainer.ts @@ -14,7 +14,7 @@ limitations under the License. */ -import { CfnOutput } from 'aws-cdk-lib'; +import { CfnOutput, Duration } from 'aws-cdk-lib'; import { ITable } from 'aws-cdk-lib/aws-dynamodb'; import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { AmiHardwareType, ContainerDefinition } from 'aws-cdk-lib/aws-ecs'; @@ -22,16 +22,27 @@ import { IRole } from 'aws-cdk-lib/aws-iam'; import { Construct } from 'constructs'; import { dump as yamlDump } from 'js-yaml'; -import { ECSCluster } from './ecsCluster'; -import { BaseProps, Ec2Metadata, EcsSourceType } from '../schema'; +import { ECSCluster, ECSTasks } from './ecsCluster'; +import { BaseProps, Ec2Metadata, ECSConfig, EcsSourceType } from '../schema'; import { Vpc } from '../networking/vpc'; -import { REST_API_PATH } from '../util'; +import { MCP_WORKBENCH_PATH, REST_API_PATH } from '../util'; import * as child_process from 'child_process'; import * as path from 'path'; +import { letIfDefined } from '../util/common-functions'; +import { Bucket } from 'aws-cdk-lib/aws-s3'; +import { StringParameter } from 'aws-cdk-lib/aws-ssm'; +import * as events from 'aws-cdk-lib/aws-events'; +import * as targets from 'aws-cdk-lib/aws-events-targets'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import { Effect, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; +import { LAMBDA_PATH } from '../util'; +import { getDefaultRuntime } from './utils'; // This is the amount of memory to buffer (or subtract off) from the total instance memory, if we don't include this, // the container can have a hard time finding available RAM resources to start and the tasks will fail deployment -const CONTAINER_MEMORY_BUFFER = 1024 * 2; +const INSTANCE_MEMORY_RESERVATION = 1024; +const SERVE_CONTAINER_MEMORY_RESERVATION = 1024 * 2; +const WORKBENCH_CONTAINER_MEMORY_RESERVATION = 1024; /** * Properties for FastApiContainer Construct. @@ -51,11 +62,11 @@ type FastApiContainerProps = { * FastApiContainer Construct. */ export class FastApiContainer extends Construct { - /** FastAPI container */ - public readonly container: ContainerDefinition; + /** Map of all container definitions by identifier */ + public readonly containers: ContainerDefinition[] = []; - /** FastAPI IAM task role */ - public readonly taskRole: IRole; + /** Map of all task roles by identifier */ + public readonly taskRoles: Partial> = {}; /** FastAPI URL **/ public readonly endpoint: string; @@ -75,7 +86,7 @@ export class FastApiContainer extends Construct { PYPI_TRUSTED_HOST: config.pypiConfig.trustedHost, LITELLM_CONFIG: yamlDump(config.litellmConfig), }; - const environment: Record = { + const baseEnvironment: Record = { LOG_LEVEL: config.logLevel, AWS_REGION: config.region, AWS_REGION_NAME: config.region, // for supporting SageMaker endpoints in LiteLLM @@ -85,18 +96,18 @@ export class FastApiContainer extends Construct { }; if (config.restApiConfig.internetFacing) { - environment.USE_AUTH = 'true'; - environment.AUTHORITY = config.authConfig!.authority; - environment.CLIENT_ID = config.authConfig!.clientId; - environment.ADMIN_GROUP = config.authConfig!.adminGroup; - environment.USER_GROUP = config.authConfig!.userGroup; - environment.JWT_GROUPS_PROP = config.authConfig!.jwtGroupsProperty; + baseEnvironment.USE_AUTH = 'true'; + baseEnvironment.AUTHORITY = config.authConfig!.authority; + baseEnvironment.CLIENT_ID = config.authConfig!.clientId; + baseEnvironment.ADMIN_GROUP = config.authConfig!.adminGroup; + baseEnvironment.USER_GROUP = config.authConfig!.userGroup; + baseEnvironment.JWT_GROUPS_PROP = config.authConfig!.jwtGroupsProperty; } else { - environment.USE_AUTH = 'false'; + baseEnvironment.USE_AUTH = 'false'; } if (tokenTable) { - environment.TOKEN_TABLE_NAME = tokenTable.tableName; + baseEnvironment.TOKEN_TABLE_NAME = tokenTable.tableName; } // Pre-generate the tiktoken cache to ensure it does not attempt to fetch data from the internet at runtime. @@ -113,71 +124,198 @@ export class FastApiContainer extends Construct { } } - const image = config.restApiConfig.imageConfig || { + const restApiImage = config.restApiConfig.imageConfig || { baseImage: config.baseImage, path: REST_API_PATH, type: EcsSourceType.ASSET }; - const apiCluster = new ECSCluster(scope, `${id}-ECSCluster`, { - config, - ecsConfig: { - amiHardwareType: AmiHardwareType.STANDARD, - autoScalingConfig: { - blockDeviceVolumeSize: 30, - minCapacity: 1, - maxCapacity: 1, - cooldown: 60, - defaultInstanceWarmup: 60, - metricConfig: { - albMetricName: 'RequestCountPerTarget', - targetValue: 1000, - duration: 60, - estimatedInstanceWarmup: 30 - } - }, - buildArgs, - containerConfig: { - image, - healthCheckConfig: { - command: ['CMD-SHELL', 'exit 0'], - interval: 10, - startPeriod: 30, - timeout: 5, - retries: 3 + const instanceType = 'm5.large'; + const healthCheckConfig = { + command: ['CMD-SHELL', 'exit 0'], + interval: 10, + startPeriod: 30, + timeout: 5, + retries: 3 + }; + const ecsConfig: ECSConfig = { + amiHardwareType: AmiHardwareType.STANDARD, + autoScalingConfig: { + blockDeviceVolumeSize: 30, + minCapacity: 1, + maxCapacity: 5, + cooldown: 60, + defaultInstanceWarmup: 60, + metricConfig: { + albMetricName: 'RequestCountPerTarget', + targetValue: 1000, + duration: 60, + estimatedInstanceWarmup: 30 + } + }, + buildArgs, + tasks: { + [ECSTasks.REST]: { + environment: baseEnvironment, + containerConfig: { + image: restApiImage, + healthCheckConfig, + environment: {}, + sharedMemorySize: 0 }, - environment: {}, - sharedMemorySize: 0 + // set a softlimit of what we expect to use + containerMemoryReservationMiB: SERVE_CONTAINER_MEMORY_RESERVATION }, - containerMemoryBuffer: CONTAINER_MEMORY_BUFFER, - environment, - identifier: props.apiName, - instanceType: 'm5.large', - internetFacing: config.restApiConfig.internetFacing, - loadBalancerConfig: { - healthCheckConfig: { - path: '/health', - interval: 60, - timeout: 30, - healthyThresholdCount: 2, - unhealthyThresholdCount: 10 + [ECSTasks.MCPWORKBENCH]: { + environment: {...baseEnvironment, + RCLONE_CONFIG_S3_REGION: config.region, + MCPWORKBENCH_BUCKET: [config.deploymentName, config.deploymentStage, 'MCPWorkbench', config.accountNumber].join('-').toLowerCase(), + }, + containerConfig: { + image: { + baseImage: config.baseImage, + path: MCP_WORKBENCH_PATH, + type: EcsSourceType.ASSET + }, + healthCheckConfig, + environment: {}, + sharedMemorySize: 0, + privileged: true }, - domainName: config.restApiConfig.domainName, - sslCertIamArn: config.restApiConfig?.sslCertIamArn ?? null, + applicationTarget: { + port: 8000, + priority: 80, + conditions: [ + { type: 'pathPatterns', values: ['/v2/mcp/*'] } + ] + }, + containerMemoryReservationMiB: WORKBENCH_CONTAINER_MEMORY_RESERVATION, + } + }, + // reserve at least enough memory for each task and a buffer for the instance to use + containerMemoryBuffer: Ec2Metadata.get(instanceType).memory - (INSTANCE_MEMORY_RESERVATION + SERVE_CONTAINER_MEMORY_RESERVATION + WORKBENCH_CONTAINER_MEMORY_RESERVATION), + instanceType, + internetFacing: config.restApiConfig.internetFacing, + loadBalancerConfig: { + healthCheckConfig: { + path: '/health', + interval: 60, + timeout: 30, + healthyThresholdCount: 2, + unhealthyThresholdCount: 10 }, + domainName: config.restApiConfig.domainName, + sslCertIamArn: config.restApiConfig?.sslCertIamArn ?? null, }, + }; + + const apiCluster = new ECSCluster(scope, `${id}-ECSCluster`, { + identifier: props.apiName, + ecsConfig, + config, securityGroup, vpc }); + const workbenchService = apiCluster.services.MCPWORKBENCH; + + // Create Lambda function to handle S3 events and trigger MCP Workbench service redeployment + const s3EventHandlerRole = new Role(this, 'S3EventHandlerRole', { + assumedBy: new ServicePrincipal('lambda.amazonaws.com'), + inlinePolicies: { + 'S3EventHandlerPolicy': new PolicyDocument({ + statements: [ + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'logs:CreateLogGroup', + 'logs:CreateLogStream', + 'logs:PutLogEvents' + ], + resources: ['arn:aws:logs:*:*:*'] + }), + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'ecs:UpdateService', + 'ecs:DescribeServices', + 'ecs:DescribeClusters' + ], + resources: [ + `arn:aws:ecs:${config.region}:*:cluster/${workbenchService?.cluster?.clusterName}*`, + `arn:aws:ecs:${config.region}:*:service/${workbenchService?.cluster?.clusterName}*/${workbenchService?.serviceName}*` + ] + }), + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'ssm:GetParameter' + ], + resources: [ + `arn:aws:ssm:${config.region}:*:parameter${config.deploymentPrefix}/deploymentName` + ] + }) + ] + }) + } + }); + + const s3EventHandlerLambda = new lambda.Function(this, 'S3EventHandlerLambda', { + runtime: getDefaultRuntime(), + handler: 'mcp_workbench.s3_event_handler.handler', + code: lambda.Code.fromAsset(config.lambdaPath ?? LAMBDA_PATH), + timeout: Duration.minutes(2), + role: s3EventHandlerRole, + environment: { + DEPLOYMENT_PREFIX: config.deploymentPrefix!, + API_NAME: props.apiName, + ECS_CLUSTER_NAME: workbenchService!.cluster?.clusterName, + MCPWORKBENCH_SERVICE_NAME: workbenchService!.serviceName + } + }); + + // Create EventBridge rule to trigger Lambda when S3 objects are created/deleted + const rescanMcpWorkbenchRule = new events.Rule(this, 'RescanMCPWorkbenchRule', { + eventPattern: { + source: ['aws.s3', 'debug'], + detailType: [ + 'Object Created', + 'Object Deleted' + ], + detail: { + bucket: { + name: [[config.deploymentName, config.deploymentStage, 'MCPWorkbench', config.accountNumber].join('-').toLowerCase()] + } + } + }, + }); + + rescanMcpWorkbenchRule.addTarget(new targets.LambdaFunction(s3EventHandlerLambda, { + retryAttempts: 2, + maxEventAge: Duration.minutes(5) + })); + if (tokenTable) { - tokenTable.grantReadData(apiCluster.taskRole); + Object.entries(apiCluster.taskRoles).forEach(([, role]) => { + tokenTable.grantReadData(role); + }); } + letIfDefined(apiCluster.taskRoles.MCPWORKBENCH, (taskRole) => { + const bucketName = [config.deploymentName, config.deploymentStage, 'MCPWorkbench', config.accountNumber].join('-').toLowerCase(); + const workbenchBucket = Bucket.fromBucketName(scope, 'MCPWorkbenchBucket', bucketName); + workbenchBucket.grantRead(taskRole); + }); + this.endpoint = apiCluster.endpointUrl; + new StringParameter(scope, 'FastApiEndpoint', { + parameterName: `${config.deploymentPrefix}/serve/endpoint`, + stringValue: this.endpoint + }); + // Update - this.container = apiCluster.container; - this.taskRole = apiCluster.taskRole; + this.containers = Object.values(apiCluster.containers); + this.taskRoles = apiCluster.taskRoles; // CFN output new CfnOutput(this, `${props.apiName}Url`, { diff --git a/lib/chat/api/configuration.ts b/lib/chat/api/configuration.ts index 2ff976940..5b9d03f30 100644 --- a/lib/chat/api/configuration.ts +++ b/lib/chat/api/configuration.ts @@ -28,6 +28,7 @@ import { Vpc } from '../../networking/vpc'; import { AwsCustomResource, PhysicalResourceId } from 'aws-cdk-lib/custom-resources'; import { IRole } from 'aws-cdk-lib/aws-iam'; import { LAMBDA_PATH } from '../../util'; +import { McpApi } from './mcp'; /** * Props for the ConfigurationApi construct @@ -45,6 +46,7 @@ type ConfigurationApiProps = { rootResourceId: string; securityGroups: ISecurityGroup[]; vpc: Vpc; + mcpApi: McpApi; } & BaseProps; /** @@ -54,7 +56,7 @@ export class ConfigurationApi extends Construct { constructor (scope: Construct, id: string, props: ConfigurationApiProps) { super(scope, id); - const { authorizer, config, restApiId, rootResourceId, securityGroups, vpc } = props; + const { authorizer, config, mcpApi, restApiId, rootResourceId, securityGroups, vpc } = props; // Get common layer based on arn from SSM due to issues with cross stack references const commonLambdaLayer = LayerVersion.fromLayerVersionArn( @@ -63,6 +65,12 @@ export class ConfigurationApi extends Construct { StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/layerVersion/common`), ); + const fastapiLambdaLayer = LayerVersion.fromLayerVersionArn( + this, + 'mcp-fastapi-lambda-layer', + StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/layerVersion/fastapi`), + ); + // Create DynamoDB table to handle config data const configTable = new dynamodb.Table(this, 'ConfigurationTable', { partitionKey: { @@ -78,7 +86,10 @@ export class ConfigurationApi extends Construct { removalPolicy: config.removalPolicy, }); + const mcpServersTable = dynamodb.Table.fromTableName(this, 'McpServersTable', mcpApi.mcpServersTableNameParameter.stringValue); + const lambdaRole: IRole = createLambdaRole(this, config.deploymentName, 'ConfigurationApi', configTable.tableArn, config.roles?.LambdaConfigurationApiExecutionRole); + mcpServersTable.grantReadWriteData(lambdaRole); // Populate the App Config table with default config const date = new Date(); @@ -107,6 +118,7 @@ export class ConfigurationApi extends Construct { 'uploadContextDocs': {'BOOL': 'True'}, 'documentSummarization': {'BOOL': 'True'}, 'showRagLibrary': {'BOOL': 'True'}, + 'showMcpWorkbench': {'BOOL': 'False'}, 'showPromptTemplateLibrary': {'BOOL': 'True'}, 'mcpConnections': {'BOOL': 'True'}, 'modelLibrary': {'BOOL': 'True'}, @@ -129,6 +141,15 @@ export class ConfigurationApi extends Construct { rootResourceId: rootResourceId, }); + const fastApiEndpoint = StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/serve/endpoint`); + + const environment = { + CONFIG_TABLE_NAME: configTable.tableName, + FASTAPI_ENDPOINT: fastApiEndpoint, + // add MCP_SERVERS_TABLE_NAME so we can update it if the configuration changes + MCP_SERVERS_TABLE_NAME: mcpServersTable.tableName + }; + // Create API Lambda functions const apis: PythonLambdaFunction[] = [ { @@ -137,9 +158,7 @@ export class ConfigurationApi extends Construct { description: 'Get configuration', path: 'configuration', method: 'GET', - environment: { - CONFIG_TABLE_NAME: configTable.tableName - }, + environment }, { name: 'update_configuration', @@ -147,9 +166,7 @@ export class ConfigurationApi extends Construct { description: 'Updates config data', path: 'configuration/{configScope}', method: 'PUT', - environment: { - CONFIG_TABLE_NAME: configTable.tableName, - }, + environment }, ]; @@ -159,7 +176,7 @@ export class ConfigurationApi extends Construct { this, restApi, lambdaPath, - [commonLambdaLayer], + [commonLambdaLayer, fastapiLambdaLayer], f, getDefaultRuntime(), vpc, diff --git a/lib/chat/api/mcp.ts b/lib/chat/api/mcp.ts index 8ef55cdab..d3ff19141 100644 --- a/lib/chat/api/mcp.ts +++ b/lib/chat/api/mcp.ts @@ -51,6 +51,8 @@ type McpApiProps = { * API which Maintains mcp state in DynamoDB */ export class McpApi extends Construct { + readonly mcpServersTableNameParameter: StringParameter; + constructor (scope: Construct, id: string, props: McpApiProps) { super(scope, id); @@ -96,6 +98,13 @@ export class McpApi extends Construct { sortKey: { name: 'created', type: dynamodb.AttributeType.STRING }, }); + // Create SSM parameter for the MCP servers table name + this.mcpServersTableNameParameter = new StringParameter(this, 'McpServersTableNameParameter', { + parameterName: `${config.deploymentPrefix}/table/mcpServersTable`, + stringValue: mcpServersTable.tableName, + description: 'Name of the MCP servers DynamoDB table', + }); + const restApi = RestApi.fromRestApiAttributes(this, 'RestApi', { restApiId: restApiId, rootResourceId: rootResourceId, diff --git a/lib/chat/chatConstruct.ts b/lib/chat/chatConstruct.ts index 8777de6b5..808a0883d 100644 --- a/lib/chat/chatConstruct.ts +++ b/lib/chat/chatConstruct.ts @@ -60,7 +60,7 @@ export class LisaChatApplicationConstruct extends Construct { vpc, }); - new ConfigurationApi(scope, 'ConfigurationApi', { + const mcpApi = new McpApi(scope, 'McpApi', { authorizer, config, restApiId, @@ -69,22 +69,23 @@ export class LisaChatApplicationConstruct extends Construct { vpc, }); - new PromptTemplateApi(scope, 'PromptTemplateApi', { + new ConfigurationApi(scope, 'ConfigurationApi', { authorizer, config, restApiId, rootResourceId, securityGroups, - vpc + vpc, + mcpApi }); - new McpApi(scope, 'McpApi', { + new PromptTemplateApi(scope, 'PromptTemplateApi', { authorizer, config, restApiId, rootResourceId, securityGroups, - vpc, + vpc }); new UserPreferencesApi(scope, 'UserPreferencesApi', { diff --git a/lib/core/apiBaseConstruct.ts b/lib/core/apiBaseConstruct.ts index 04b123028..7cb27dcc4 100644 --- a/lib/core/apiBaseConstruct.ts +++ b/lib/core/apiBaseConstruct.ts @@ -23,6 +23,7 @@ import { BaseProps } from '../schema'; import { Vpc } from '../networking/vpc'; import { Role } from 'aws-cdk-lib/aws-iam'; import { ITable } from 'aws-cdk-lib/aws-dynamodb'; +import McpWorkbenchConstruct from '../serve/mcpWorkbenchConstruct'; export type LisaApiBaseProps = { vpc: Vpc; @@ -79,6 +80,14 @@ export class LisaApiBaseConstruct extends Construct { binaryMediaTypes: ['font/*', 'image/*'], }); + new McpWorkbenchConstruct(this, id + 'McpWorkbench', { + ...props, + authorizer: this.authorizer!, + restApiId: restApi.restApiId, + rootResourceId: restApi.restApiRootResourceId, + securityGroups: [props.vpc.securityGroups.ecsModelAlbSg], + }); + this.restApi = restApi; this.restApiId = restApi.restApiId; this.rootResourceId = restApi.restApiRootResourceId; diff --git a/lib/docs/.vitepress/config.mts b/lib/docs/.vitepress/config.mts index 27516e2fc..3d5684436 100644 --- a/lib/docs/.vitepress/config.mts +++ b/lib/docs/.vitepress/config.mts @@ -67,6 +67,7 @@ const navLinks = [ ], }, { text: 'Model Context Protocol (MCP)', link: '/config/mcp' }, + { text: 'MCP Workbench', link: '/config/mcp-workbench' }, { text: 'Usage Analytics', link: '/config/cloudwatch' }, ], }, diff --git a/lib/docs/config/mcp-workbench.md b/lib/docs/config/mcp-workbench.md new file mode 100644 index 000000000..2a075f74c --- /dev/null +++ b/lib/docs/config/mcp-workbench.md @@ -0,0 +1,154 @@ +# MCP Workbench + +The MCP Workbench is a development environment that enables administrators to create, test, and deploy custom tools through LISA's hosted Model Context Protocol (MCP) server. This feature provides a browser-based Python editor for rapid prototyping and deployment of custom functionality. + +> **Note:** For comprehensive information about the Model Context Protocol, please refer to the [Model Context Protocol (MCP)](./mcp.md) documentation. + +## Overview + +The MCP Workbench serves as an introduction to hosted tool development within the MCP ecosystem. It provides administrators with the capability to: + +- Create custom Python-based tools accessible to all users +- Test and iterate on tool functionality in real-time +- Deploy tools seamlessly through LISA's MCP server infrastructure +- Share tools across the organization without complex deployment processes + +The integrated browser-based editor allows administrators to write Python code and expose functions as MCP tools by using simple annotations or class extensions. + +## Prerequisites + +- Administrator privileges in LISA +- MCP Server Connections feature enabled +- Basic understanding of Python programming +- Familiarity with MCP concepts (recommended) + +## Configuration + +### Step 1: Enable the MCP Workbench Menu + +1. **Access Admin Configuration** + - Navigate to the Admin menu + - Select "Configuration" + +2. **Enable Required Features** + - Ensure "MCP Server Connections" is enabled + - Enable "Show MCP Workbench" + - Save the configuration + +This configuration creates a new menu item in the administrators section, providing access to the Python file editor for tool creation, modification, and deletion. + +### Step 2: Activate the MCP Server Connection + +Enabling the MCP Workbench automatically creates a new MCP Connection to LISA's hosted MCP server. This connection must be activated to make the tools available in the chat application. + +1. **Navigate to MCP Connections** + - Go to the Libraries menu + - Select "MCP Connections" + +2. **Activate the Connection** + - Locate the "MCP Workbench" connection + - Click the radio button to select it + - Choose "Edit" from the actions menu + - Toggle the "Active" setting to enabled + - Save the configuration + +> **Security Note:** The MCP Workbench connection uses a special Bearer Token with the placeholder `{LISA_BEARER_TOKEN}`, which is automatically replaced with each user's individual OIDC token for secure access. + +## Usage + +### Chat Interface Integration + +Once the MCP Workbench connection is activated, all custom enabled tools become immediately available in the chat interface. Users can discover and utilize these tools through the standard MCP tool invocation methods within their conversations. + +### Programmatic API Access + +LISA automatically hosts an MCP Server containing all MCP Workbench tools. The server is accessible through the following endpoints: + +**AWS Load Balancer URL:** +``` +https://abc-rest-..elb.amazonaws.com/v2/mcp/ +``` + +**Custom Domain URL (if configured):** +``` +https:///v2/mcp/ +``` + +> **Authentication Required:** API access requires [Programmatic API Tokens](./api-tokens.md) for authentication. + +## Development Guidelines + +### Creating Your First Tool + +Tools can be created using two simple approaches: + +#### Function-based Tool (Annotation Method) + +```python +from mcpworkbench.core.annotations import mcp_tool +from typing import Annotated + +@mcp_tool( + name="hello_world", + description="A personalized greeting to the a person." +) +def hello_world(name: Annotated[str, "The name of the person to greet."]) -> str: + """ + A simple hello world tool that greets a user. + + Args: + name: The name of the person to greet + + Returns: + A greeting message + """ + return f"Hello, {name}! Welcome to MCP Workbench." +``` + +> **Note:** Using `Annotated` on function parameters is optional. You can use standard Python type hints if preferred. + +#### Class-based Tool (Inherited Class Method) + +```python +from mcpworkbench.tools import BaseTool +from typing import Annotated + +class HelloWorldTool(BaseTool): + """A simple hello world tool using class inheritance.""" + + def __init__(self): + """ + Initialize the tool with metadata. + + The BaseTool constructor requires: + - name: A unique identifier for the tool + - description: A clear description of what the tool does + """ + super().__init__( + name = "hello_world_class", + description = "A class-based hello world tool" + ) + + async def execute(self): + return self.greet + + async def greet(self, name: Annotated[str, "The name of the person to greet."]) -> str: + """ + Execute the hello world functionality. + + Args: + name: The name of the person to greet + + Returns: + A greeting message + """ + return f"Hello, {name}! This is from a class-based tool." +``` + +Both approaches will make your tool available in the chat interface once deployed. + +## Advanced Usage + +### Adding Python Dependencies + +Operators can modify `lib/serve/mcp-workbench/requirements.txt` to add additional Python libraries that will be available in the MCP Workbench environment. After modifying the requirements file, you'll need to perform a CDK deployment for those additional libraries to become available to your custom tools. diff --git a/lib/schema/cdk.ts b/lib/schema/cdk.ts index 148996f66..80fab00df 100644 --- a/lib/schema/cdk.ts +++ b/lib/schema/cdk.ts @@ -58,4 +58,35 @@ export enum AmiHardwareType { NEURON = 'Neuron' } +/** + * Load balancing protocol for application load balancers + */ +export enum ApplicationProtocol { + /** + * HTTP + */ + HTTP = 'HTTP', + /** + * HTTPS + */ + HTTPS = 'HTTPS' +} +/** + * Load balancing protocol version for application load balancers + */ +export enum ApplicationProtocolVersion { + /** + * GRPC + */ + GRPC = 'GRPC', + /** + * HTTP1 + */ + HTTP1 = 'HTTP1', + /** + * HTTP2 + */ + HTTP2 = 'HTTP2' +} + /* eslint-enable @typescript-eslint/no-duplicate-enum-values */ diff --git a/lib/schema/configSchema.ts b/lib/schema/configSchema.ts index e5a5176aa..c29fd4c76 100644 --- a/lib/schema/configSchema.ts +++ b/lib/schema/configSchema.ts @@ -15,7 +15,7 @@ */ import { z } from 'zod'; -import { AmiHardwareType, EcsSourceType, RemovalPolicy } from './cdk'; +import { AmiHardwareType, ApplicationProtocol, ApplicationProtocolVersion, EcsSourceType, RemovalPolicy } from './cdk'; import { RagRepositoryConfigSchema, RdsInstanceConfig } from './ragSchema'; /** @@ -460,8 +460,11 @@ export const ContainerConfigSchema = z.object({ .describe('Environment variables for the container.'), sharedMemorySize: z.number().min(0).default(0).describe('The value for the size of the /dev/shm volume.'), healthCheckConfig: ContainerHealthCheckConfigSchema.default({}), + privileged: z.boolean().optional() }).describe('Configuration for the container.'); +export type ContainerConfig = z.infer; + const HealthCheckConfigSchema = z.object({ path: z.string().describe('Path for the health check.'), interval: z.number().default(30).describe('Interval in seconds between health checks.'), @@ -496,6 +499,38 @@ export const AutoScalingConfigSchema = z.object({ }) .describe('Configuration for auto scaling settings.'); +const enumKeySchema = >(e: E) => { + const keys = Object.keys(e).filter((k) => Number.isNaN(Number(k))) as Array>; + if (keys.length === 0) { + throw new Error('Enum must have at least one valid key'); + } + return z.enum(keys as unknown as [string, ...string[]]); +}; + +export const ApplicationTargetSchema = z.object({ + protocol: enumKeySchema(ApplicationProtocol).optional(), + protocolVersion: enumKeySchema(ApplicationProtocolVersion).optional(), + port: z.number().positive().optional(), + priority: z.number().optional(), + conditions: z.array(z.object({ + type: z.enum(['pathPatterns']), + values: z.array(z.string()) + })).optional() +}); + +export type ApplicationTarget = z.infer; + +export const TaskDefinitionSchema = z.object({ + containerConfig: ContainerConfigSchema, + containerMemoryReservationMiB: z.number().default(1024 * 2) + .describe('The amount (in MiB) of memory to present to the container.').optional(), + memoryLimitMiB: z.number().positive().describe('The amount (in MiB) of memory to present to the container.').optional(), + environment: z.record(z.string()).describe('Environment variables set on the task container'), + applicationTarget: ApplicationTargetSchema.optional().describe('How the load balancer should target the task.') +}); + +export type TaskDefinition = z.infer; + /** * Configuration schema for an ECS model. @@ -520,15 +555,13 @@ export const EcsBaseConfigSchema = z.object({ autoScalingConfig: AutoScalingConfigSchema.describe('Configuration for auto scaling settings.'), buildArgs: z.record(z.string()).optional() .describe('Optional build args to be applied when creating the task container if containerConfig.image.type is ASSET'), - containerConfig: ContainerConfigSchema, containerMemoryBuffer: z.number().default(1024 * 2) - .describe('This is the amount of memory to buffer (or subtract off) from the total instance memory, ' + + .describe('This is the amount of memory to buffer (or subtract off) from the total instance memory, ' + 'if we don\'t include this, the container can have a hard time finding available RAM resources to start and the tasks will fail deployment'), - environment: z.record(z.string()).describe('Environment variables set on the task container'), - identifier: z.string(), + tasks: z.record(TaskDefinitionSchema), instanceType: z.enum(VALID_INSTANCE_KEYS).describe('EC2 instance type for running the model.'), internetFacing: z.boolean().default(false).describe('Whether or not the cluster will be configured as internet facing'), - loadBalancerConfig: LoadBalancerConfigSchema, + loadBalancerConfig: LoadBalancerConfigSchema }); @@ -551,16 +584,10 @@ export type RegisteredModel = { streaming?: boolean; }; - /** * Type representing configuration for an ECS model. */ -type EcsBaseConfig = z.infer; - -/** - * Union model type representing various model configurations. - */ -export type ECSConfig = EcsBaseConfig; +export type ECSConfig = z.infer; /** * Configuration schema for an ECS model. diff --git a/lib/serve/mcp-workbench/.gitignore b/lib/serve/mcp-workbench/.gitignore new file mode 100644 index 000000000..510c73d0f --- /dev/null +++ b/lib/serve/mcp-workbench/.gitignore @@ -0,0 +1,114 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/lib/serve/mcp-workbench/Dockerfile b/lib/serve/mcp-workbench/Dockerfile new file mode 100644 index 000000000..7ddc3a087 --- /dev/null +++ b/lib/serve/mcp-workbench/Dockerfile @@ -0,0 +1,63 @@ +FROM python:3.12-slim + +ARG RCLONE_VERSION=v1.71.0 +ARG RCLONE_ARCH=amd64 +ARG S6_OVERLAY_VERSION=3.1.6.2 +ARG S6_OVERLAY_ARCH=x86_64 + +ARG S6_OVERLAY_NOARCH_SOURCE="https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" +ENV S6_OVERLAY_NOARCH_SOURCE=$S6_OVERLAY_NOARCH_SOURCE +ARG S6_OVERLAY_ARCH_SOURCE="https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_OVERLAY_ARCH}.tar.xz" +ENV S6_OVERLAY_ARCH_SOURCE=$S6_OVERLAY_ARCH_SOURCE +ARG RCLONE_SOURCE="https://github.com/rclone/rclone/releases/download/${RCLONE_VERSION}/rclone-${RCLONE_VERSION}-linux-${RCLONE_ARCH}.zip" +ENV RCLONE_SOURCE=$RCLONE_SOURCE + +WORKDIR /workspace + +RUN apt-get update && apt-get install -y \ + curl \ + fuse3 \ + unzip \ + xz-utils \ + && rm -rf /var/lib/apt/lists/* + +# Install s6-overlay +ADD $S6_OVERLAY_NOARCH_SOURCE /tmp/ +ADD $S6_OVERLAY_ARCH_SOURCE /tmp/ +RUN if [ -f "/tmp/$(basename $S6_OVERLAY_NOARCH_SOURCE)" ]; then tar -C / -Jxpf "/tmp/$(basename $S6_OVERLAY_NOARCH_SOURCE)"; fi && \ + if [ -f "/tmp/$(basename $S6_OVERLAY_ARCH_SOURCE)" ]; then tar -C / -Jxpf "/tmp/$(basename $S6_OVERLAY_ARCH_SOURCE)"; fi && \ + (rm /tmp/s6-overlay-*.tar.xz || true) + +# Install rclone +ADD $RCLONE_SOURCE /tmp +RUN unzip -d /tmp /tmp/$(basename $RCLONE_SOURCE) && \ + install --owner root --group root --mode 755 /tmp/$(basename ${RCLONE_SOURCE%.*})/rclone /usr/bin/ && \ + rm -rf /tmp/rclone-* + +# Copy and install the MCP workbench package +COPY . /workspace/mcpworkbench-src/ +RUN pip install -e /workspace/mcpworkbench-src/ + +# Install additional requirements +COPY requirements.txt /workspace/ +RUN pip install -r /workspace/requirements.txt + +# Copy s6-overlay service definitions +COPY s6-overlay/services.d /etc/services.d +RUN chmod +x /etc/services.d/mcpworkbench/run +# RUN ln -s ../supervise/notify /etc/services.d/s3mount/notification-fd + +# Create tools directory +RUN mkdir -p /workspace/tools + +# Set default environment variables +ENV TOOLS_DIR=/workspace/tools +ENV HOST=0.0.0.0 +ENV PORT=8000 +ENV MCP_ROUTE=/v2/mcp +ENV CORS_ORIGINS=* +ENV LOG_LEVEL=info + +EXPOSE 8000 + +ENTRYPOINT ["/init"] diff --git a/lib/serve/mcp-workbench/README.md b/lib/serve/mcp-workbench/README.md new file mode 100644 index 000000000..6b73d18b4 --- /dev/null +++ b/lib/serve/mcp-workbench/README.md @@ -0,0 +1,360 @@ +# MCP Workbench + +A dynamic host for Python files used as MCP (Model Context Protocol) tools. MCP Workbench allows you to dynamically load and serve Python tools as MCP tools through a pure FastMCP 2.0 server. + +## Features + +- **Dynamic Tool Discovery**: Automatically discovers tools from Python files in a configurable directory +- **Two Tool Types**: Supports both class-based tools (inheriting from `BaseTool`) and function-based tools (using `@mcp_tool` decorator) +- **Pure MCP Protocol**: Native MCP protocol implementation via FastMCP 2.0 +- **Hot Reloading**: HTTP GET endpoint to rescan and reload tools without server restart +- **Professional Module Loading**: Uses `importlib` and `inspect` for safe module analysis +- **Configurable**: Support for YAML configuration files and CLI arguments +- **Better Parameter Support**: Leverages FastMCP 2.0's improved JSON schema handling +- **Clean Architecture**: Simplified single-protocol design + +## Installation + +```bash +# Install from the project directory +cd lib/serve/mcp-workbench +pip install -e . +``` + +## Quick Start + +1. **Create a tools directory with some example tools:** + +```bash +mkdir /tmp/my-tools +``` + +2. **Create a simple tool (save as `/tmp/my-tools/hello.py`):** + +```python +from mcpworkbench.core.annotations import mcp_tool + +@mcp_tool( + name="hello", + description="Say hello to someone", + parameters={ + "type": "object", + "properties": { + "name": {"type": "string", "description": "Name to greet"} + }, + "required": ["name"] + } +) +def say_hello(name: str): + return f"Hello, {name}!" +``` + +3. **Start the server:** + +```bash +mcpworkbench --tools-dir /tmp/my-tools --port 8000 +``` + +4. **Connect with an MCP client:** + +The server exposes a pure MCP protocol endpoint that MCP clients can connect to for tool discovery and execution. + +## Tool Development + +### Class-Based Tools + +Create tools by inheriting from `BaseTool`: + +```python +from mcpworkbench.core.base_tool import BaseTool + +class MyTool(BaseTool): + def __init__(self): + super().__init__( + name="my_tool", + description="Description of what my tool does" + ) + + def get_parameters(self): + return { + "type": "object", + "properties": { + "param1": {"type": "string", "description": "First parameter"} + }, + "required": ["param1"] + } + + async def execute(self, **kwargs): + param1 = kwargs["param1"] + return {"result": f"Processed: {param1}"} +``` + +### Function-Based Tools + +Create tools using the `@mcp_tool` decorator: + +```python +from mcpworkbench.core.annotations import mcp_tool + +@mcp_tool( + name="my_function_tool", + description="A function-based tool", + parameters={ + "type": "object", + "properties": { + "text": {"type": "string", "description": "Input text"} + }, + "required": ["text"] + } +) +async def process_text(text: str): + return {"processed": text.upper()} +``` + +## Configuration + +### Command Line Usage + +```bash +mcpworkbench [OPTIONS] + +Options: + -c, --config PATH Path to YAML configuration file + -t, --tools-dir PATH Directory containing tool files + --host TEXT Server host address (default: 127.0.0.1) + -p, --port INTEGER Server port (default: 8000) + --exit-route TEXT Enable exit_server HTTP GET endpoint (optional) + --rescan-route TEXT Enable rescan_tools HTTP GET endpoint (optional) + --cors-origins TEXT Comma-separated list of allowed CORS origins + -v, --verbose Enable verbose logging + --debug Enable debug logging +``` + +### YAML Configuration + +```yaml +# Server settings +host: "0.0.0.0" +port: 8000 + +# Tool settings +tools_dir: "/path/to/tools" + +# Management routes (optional) +exit_route: "/shutdown" # Enables exit_server HTTP GET endpoint +rescan_route: "/rescan" # Enables rescan_tools HTTP GET endpoint + +# CORS settings (simple format) +cors_origins: ["*"] + +# Advanced CORS settings (optional - will use defaults if not specified) +cors_settings: + allow_methods: ["GET", "POST", "OPTIONS"] + allow_headers: ["*"] + allow_credentials: false + expose_headers: [] + max_age: 600 +``` + +## Architecture + +**Pure FastMCP 2.0 Server:** +- **Native MCP Protocol**: 100% MCP protocol implementation via FastMCP 2.0 +- **Dynamic Tool Registration**: Tools discovered and registered as native FastMCP tools +- **Management Routes**: Rescan and exit functionality as HTTP GET endpoints +- **Better Parameter Support**: Leverages FastMCP 2.0's improved JSON schema handling +- **Simplified Codebase**: Single protocol, no adapter layer needed + +## MCP Tools + +### Discovered Tools + +All Python tools from your tools directory are automatically registered as MCP tools with their: +- Original names and descriptions +- Parameter schemas +- Execution capabilities + +### Built-in Management Routes + +**GET /rescan** (when enabled): +- Rescans the tools directory for new/updated tools +- Returns JSON status of changes made +- Accessible via HTTP GET requests + +**GET /shutdown** (when enabled): +- Gracefully shuts down the MCP Workbench server +- Returns JSON confirmation before shutdown +- Useful for remote management + +## Example Usage + +### Using an MCP Client + +```python +# Example MCP client usage (pseudocode) +import mcp_client + +client = mcp_client.connect("http://localhost:8000") + +# List available tools +tools = client.list_tools() +print(f"Available tools: {[tool.name for tool in tools]}") + +# Call a tool +result = client.call_tool("hello", {"name": "World"}) +print(f"Result: {result}") + +# Rescan for new tools via HTTP GET (if enabled) +import requests +rescan_result = requests.get("http://localhost:8000/rescan") +print(f"Rescan result: {rescan_result.json()}") +``` + +## Docker Usage + +The project includes a Dockerfile for containerized deployment using s6-overlay for service management: + +```bash +# Build the container +docker build -t mcp-workbench lib/serve/mcp-workbench/ + +# Run with mounted tools directory and environment variables +docker run -v /path/to/tools:/workspace/tools \ + -p 8000:8000 \ + -e TOOLS_DIR=/workspace/tools \ + -e RESCAN_ROUTE=/rescan \ + -e LOG_LEVEL=debug \ + mcp-workbench +``` + +### Environment Variables + +The container supports the following environment variables: + +- `TOOLS_DIR` - Directory containing tool files (default: `/workspace/tools`) +- `HOST` - Server host address (default: `0.0.0.0`) +- `PORT` - Server port (default: `8000`) +- `RESCAN_ROUTE` - Enable rescan_tools HTTP GET endpoint (optional) +- `EXIT_ROUTE` - Enable exit_server HTTP GET endpoint (optional) +- `CORS_ORIGINS` - Comma-separated list of allowed CORS origins (default: `*`) +- `LOG_LEVEL` - Logging level: `info`, `verbose`, or `debug` (default: `info`) + +### s6-overlay Service Management + +The container uses s6-overlay to manage the MCP workbench service as a long-running process. This provides: + +- **Automatic restart** if the service crashes +- **Proper signal handling** for graceful shutdown +- **Service dependency management** +- **Logging and monitoring capabilities** + +The service is configured in `/etc/s6-overlay/s6-rc.d/mcpworkbench/` and starts automatically when the container launches. + +## AWS Integration + +This project is designed to work with the existing LISA MCP infrastructure: + +1. Tools are created/edited via the LISA web interface +2. Tools are stored in S3 via the existing Lambda functions +3. S3 bucket is mounted to the container filesystem +4. MCP Workbench reads tools from the mounted location +5. External processes can trigger rescans via HTTP GET requests + +## Development + +### Project Structure + +``` +src/mcpworkbench/ +├── __init__.py +├── cli.py # Command line interface +├── config/ +│ ├── __init__.py +│ └── models.py # Configuration data models +├── core/ +│ ├── __init__.py +│ ├── base_tool.py # BaseTool abstract class +│ ├── tool_discovery.py # Tool discovery component +│ ├── tool_registry.py # Tool registry component +│ └── annotations.py # Tool function annotations +├── server/ +│ ├── __init__.py +│ ├── mcp_server.py # FastMCP 2.0 server implementation +│ └── middleware.py # Legacy middleware (may be removed) +└── adapters/ + ├── __init__.py + └── tool_adapter.py # Legacy adapters (may be removed) +``` + +### Testing + +#### Pytest Tests + +The project includes comprehensive pytest-based tests: + +```bash +# Install with development dependencies +cd lib/serve/mcp-workbench +pip install -e ".[dev]" + +# Alternative if above doesn't work: +pip install -e . +pip install pytest pytest-asyncio requests pytest-timeout + +# Run all tests +pytest + +# Run tests with verbose output +pytest -v + +# Run specific test files +pytest tests/test_core.py +pytest tests/test_adapters.py +pytest tests/test_integration.py + +# Run tests with coverage +pytest --cov=mcpworkbench + +# Run only unit tests (exclude integration tests) +pytest tests/test_core.py tests/test_adapters.py +``` + +#### Manual Testing + +For manual testing and interactive server exploration: + +```bash +# Run the manual test script (includes API testing) +python tests/test_manual.py + +# Or run with example tools +mcpworkbench --tools-dir src/examples/sample_tools --port 8001 --debug +``` + +#### Test Structure + +- `tests/test_core.py` - Unit tests for core components (BaseTool, annotations, discovery, registry) +- `tests/test_adapters.py` - Unit tests for tool adapters +- `tests/test_integration.py` - Full integration tests with running server +- `tests/test_manual.py` - Interactive manual testing script +- `tests/conftest.py` - Pytest fixtures and configuration + +## Migration from Hybrid Architecture + +If migrating from the previous hybrid REST API + MCP architecture: + +### What Changed +- **Management as HTTP Routes**: Rescan/exit are now HTTP GET endpoints, not MCP tools +- **Added Dependencies**: Added Starlette/Uvicorn dependencies for HTTP route support +- **Hybrid Architecture**: MCP protocol for tools + HTTP GET endpoints for management +- **FastMCP 2.0**: Better parameter support and native MCP integration + +### What Stayed the Same +- **Tool Discovery**: Same Python file scanning and tool detection +- **Tool Types**: Both class-based and function-based tools still supported +- **Configuration**: Same YAML and CLI configuration options +- **Docker Support**: Same containerization and s6-overlay integration + +## License + +Licensed under the Apache License, Version 2.0. See the LICENSE file for details. diff --git a/lib/serve/mcp-workbench/pyproject.toml b/lib/serve/mcp-workbench/pyproject.toml new file mode 100644 index 000000000..5dfffe655 --- /dev/null +++ b/lib/serve/mcp-workbench/pyproject.toml @@ -0,0 +1,55 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "mcpworkbench" +version = "0.1.0" +description = "A dynamic host for python files used as MCP tools" +authors = [{name = "Dustin Sweigart", email = "dustinps@amazon.com"}] +dependencies = [ + "fastmcp>=2.0.0", + "pydantic>=2.0.0", + "pyyaml>=6.0.0", + "click>=8.0.0", + "starlette>=0.28.0", + "uvicorn>=0.23.0", + "aioboto3==12.3.0", + "aiobotocore==2.11.2", + "aiohttp==3.10.11", + "boto3==1.34.34", + "cryptography==43.0.3", + "gunicorn==23.0.0", + "pydantic==2.8.2", + "PyJWT==2.9.0", + "requests==2.32.4", + "fastapi==0.115.11", + "fastapi_utils==0.7.0", + "loguru==0.7.2" +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "pytest-asyncio>=0.21.0", + "requests>=2.28.0", + "pytest-timeout>=2.1.0", +] + +[project.scripts] +mcpworkbench = "mcpworkbench.cli:main" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-dir] +"" = "src" + +[tool.pytest.ini_options] +minversion = "7.0" +addopts = "-ra -q --tb=short" +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +asyncio_mode = "auto" diff --git a/lib/serve/mcp-workbench/requirements.txt b/lib/serve/mcp-workbench/requirements.txt new file mode 100644 index 000000000..b4c6d5a87 --- /dev/null +++ b/lib/serve/mcp-workbench/requirements.txt @@ -0,0 +1,2 @@ +## Add additional requirements to this file +## boto3==1.34.34 diff --git a/lib/serve/mcp-workbench/s6-overlay/services.d/mcpworkbench/run b/lib/serve/mcp-workbench/s6-overlay/services.d/mcpworkbench/run new file mode 100755 index 000000000..308f62260 --- /dev/null +++ b/lib/serve/mcp-workbench/s6-overlay/services.d/mcpworkbench/run @@ -0,0 +1,52 @@ +#!/command/with-contenv bash + +# Get environment variables with defaults +TOOLS_DIR="${TOOLS_DIR:-/workspace/tools}" +HOST="${HOST:-0.0.0.0}" +PORT="${PORT:-8000}" +RESCAN_ROUTE="${RESCAN_ROUTE:-/rescan}" +EXIT_ROUTE="${EXIT_ROUTE:-/exit}" +CORS_ORIGINS="${CORS_ORIGINS:-*}" +LOG_LEVEL="${LOG_LEVEL:-info}" + +# Build command arguments +ARGS="--tools-dir ${TOOLS_DIR} --host ${HOST} --port ${PORT}" + +# Add optional routes if set +if [ -n "${RESCAN_ROUTE}" ]; then + ARGS="${ARGS} --rescan-route ${RESCAN_ROUTE}" +fi + +if [ -n "${EXIT_ROUTE}" ]; then + ARGS="${ARGS} --exit-route ${EXIT_ROUTE}" +fi + +# Add CORS origins +if [ -n "${EXIT_ROUTE}" ]; then + ARGS="${ARGS} --cors-origins \"${CORS_ORIGINS}\"" +fi + +# Add verbosity based on log level +case "${LOG_LEVEL}" in + debug) + ARGS="${ARGS} --debug" + ;; + verbose) + ARGS="${ARGS} --verbose" + ;; +esac + +# Log startup +echo "[mcpworkbench] Starting MCP Workbench server..." +echo "[mcpworkbench] Tools directory: ${TOOLS_DIR}" +echo "[mcpworkbench] Server: ${HOST}:${PORT}" +echo "[mcpworkbench] MCP route: ${MCP_ROUTE}" +echo "[mcpworkbench] Arguments: ${ARGS}" + +# Create tools directory if it doesn't exist +mkdir -p "${TOOLS_DIR}" + +s6-svwait -U /run/service/s3mount + +# Start the MCP workbench server +exec s6-setuidgid root mcpworkbench ${ARGS} diff --git a/lib/serve/mcp-workbench/s6-overlay/services.d/mcpworkbench/type b/lib/serve/mcp-workbench/s6-overlay/services.d/mcpworkbench/type new file mode 100644 index 000000000..5883cff0c --- /dev/null +++ b/lib/serve/mcp-workbench/s6-overlay/services.d/mcpworkbench/type @@ -0,0 +1 @@ +longrun diff --git a/lib/serve/mcp-workbench/s6-overlay/services.d/s3mount/data/check b/lib/serve/mcp-workbench/s6-overlay/services.d/s3mount/data/check new file mode 100755 index 000000000..051108a0c --- /dev/null +++ b/lib/serve/mcp-workbench/s6-overlay/services.d/s3mount/data/check @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +TOOLS_DIR="${TOOLS_DIR:-/workspace/tools}" + +mountpoint $TOOLS_DIR diff --git a/lib/serve/mcp-workbench/s6-overlay/services.d/s3mount/notification-fd b/lib/serve/mcp-workbench/s6-overlay/services.d/s3mount/notification-fd new file mode 100644 index 000000000..00750edc0 --- /dev/null +++ b/lib/serve/mcp-workbench/s6-overlay/services.d/s3mount/notification-fd @@ -0,0 +1 @@ +3 diff --git a/lib/serve/mcp-workbench/s6-overlay/services.d/s3mount/run b/lib/serve/mcp-workbench/s6-overlay/services.d/s3mount/run new file mode 100755 index 000000000..ef7e48a51 --- /dev/null +++ b/lib/serve/mcp-workbench/s6-overlay/services.d/s3mount/run @@ -0,0 +1,32 @@ +#!/command/with-contenv bash + +# Get environment variables with defaults +TOOLS_DIR="${TOOLS_DIR:-/workspace/tools}" +LOG_LEVEL="${LOG_LEVEL:-info}" +MCPWORKBENCH_BUCKET="${MCPWORKBENCH_BUCKET:-}" + +# Build command arguments +ARGS="--allow-other --read-only --vfs-cache-mode off --direct-io --dir-cache-time 0s --attr-timeout 0s --poll-interval 0s :s3,provider=AWS,env_auth:${MCPWORKBENCH_BUCKET} ${TOOLS_DIR}" + +# Add verbosity based on log level +case "${LOG_LEVEL}" in + debug) + ARGS="${ARGS} --debug-fuse" + ;; + verbose) + ARGS="${ARGS} --debug-fuse -vv" + ;; +esac + +# Log startup +echo "[rclone] Starting rclone mount of S3 ${MCPWORKBENCH_BUCKET}..." +echo "[rclone] Mounting to ${TOOLS_DIR}" +echo "[rclone] Arguments: ${ARGS}" + +# Create tools directory if it doesn't exist +mkdir -p "${TOOLS_DIR}" + +# s6-notifyoncheck -d 10000 -c "mountpoint -q /workspace/tools" + +# Start the rclone daemon +exec s6-notifyoncheck s6-setuidgid root rclone mount ${ARGS} diff --git a/lib/serve/mcp-workbench/s6-overlay/services.d/s3mount/type b/lib/serve/mcp-workbench/s6-overlay/services.d/s3mount/type new file mode 100644 index 000000000..5883cff0c --- /dev/null +++ b/lib/serve/mcp-workbench/s6-overlay/services.d/s3mount/type @@ -0,0 +1 @@ +longrun diff --git a/lib/serve/mcp-workbench/src/examples/sample_tools/calculator_tool.py b/lib/serve/mcp-workbench/src/examples/sample_tools/calculator_tool.py new file mode 100644 index 000000000..31085baea --- /dev/null +++ b/lib/serve/mcp-workbench/src/examples/sample_tools/calculator_tool.py @@ -0,0 +1,213 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +MCP Tool Creation Tutorial +========================== + +This file demonstrates how to create MCP (Model Context Protocol) tools using two different approaches: +1. Class-based method (shown in this file) +2. Function-based method with @mcp_tool decorator (shown in comments below) + +Both methods allow you to create tools that can be called by AI models to perform specific tasks. +""" + +from typing import Annotated + +from mcpworkbench.core.base_tool import BaseTool + +# ============================================================================= +# METHOD 1: CLASS-BASED APPROACH +# ============================================================================= +# This is the more structured approach, ideal for complex tools that need +# initialization, state management, or multiple related operations. + + +class CalculatorTool(BaseTool): + """ + A simple calculator tool that performs basic arithmetic operations. + + This class demonstrates the class-based approach to creating MCP tools: + 1. Inherit from BaseTool + 2. Initialize with name and description in __init__ + 3. Implement execute() method that returns the callable function + 4. Define the actual tool function with proper type annotations + """ + + def __init__(self): + """ + Initialize the tool with metadata. + + The BaseTool constructor requires: + - name: A unique identifier for the tool + - description: A clear description of what the tool does + """ + super().__init__( + name="calculator", description="Performs basic arithmetic operations (add, subtract, multiply, divide)" + ) + + async def execute(self): + """ + Return the callable function that implements the tool's functionality. + + This method is called by the MCP framework to get the actual function + that will be executed when the tool is invoked. + """ + return self.calculate + + async def calculate( + self, + operator: Annotated[str, "add, subtract, multiply, or divide"], + left_operand: Annotated[float, "The first number"], + right_operand: Annotated[float, "The second number"], + ): + """ + Execute the calculator operation. + + Parameter Type Annotations with Context: + ======================================= + Notice the use of Annotated[type, "description"] for each parameter. + This is OPTIONAL but highly recommended because it provides: + + 1. Type information for the MCP framework + 2. Human-readable descriptions that help AI models understand + what each parameter is for + 3. Better error messages and validation + + The Annotated type comes from typing module and follows this pattern: + Annotated[actual_type, "description_string"] + + Examples: + - Annotated[str, "The operation to perform"] + - Annotated[int, "A positive integer between 1 and 100"] + - Annotated[list[str], "A list of file paths to process"] + """ + if operator == "add": + result = left_operand + right_operand + elif operator == "subtract": + result = left_operand - right_operand + elif operator == "multiply": + result = left_operand * right_operand + elif operator == "divide": + if right_operand == 0: + raise ValueError("Cannot divide by zero") + result = left_operand / right_operand + else: + raise ValueError(f"Unknown operator: {operator}") + + return {"operator": operator, "left_operand": left_operand, "right_operand": right_operand, "result": result} + + +# ============================================================================= +# METHOD 2: FUNCTION-BASED APPROACH WITH @mcp_tool DECORATOR +# ============================================================================= +# This is a simpler approach for straightforward tools that don't need +# complex initialization or state management. + +""" +Here's how you would implement the same calculator using the @mcp_tool decorator: + +from mcpworkbench.core.decorators import mcp_tool +from typing import Annotated + +@mcp_tool( + name="simple_calculator", + description="A simple calculator using the decorator approach" +) +async def simple_calculator( + operator: Annotated[str, "The arithmetic operation: add, subtract, multiply, or divide"], + left_operand: Annotated[float, "The first number in the operation"], + right_operand: Annotated[float, "The second number in the operation"] +) -> dict: + ''' + Perform basic arithmetic operations using the decorator approach. + + The @mcp_tool decorator automatically: + 1. Registers the function as an MCP tool + 2. Extracts parameter information from type annotations + 3. Uses the Annotated descriptions for parameter documentation + 4. Handles the MCP protocol communication + + This approach is ideal for: + - Simple, stateless operations + - Quick prototyping + - Tools that don't need complex initialization + ''' + + if operator == "add": + result = left_operand + right_operand + elif operator == "subtract": + result = left_operand - right_operand + elif operator == "multiply": + result = left_operand * right_operand + elif operator == "divide": + if right_operand == 0: + raise ValueError("Cannot divide by zero") + result = left_operand / right_operand + else: + raise ValueError(f"Unknown operator: {operator}") + + return { + "operator": operator, + "left_operand": left_operand, + "right_operand": right_operand, + "result": result + } + +# Additional examples of Annotated usage for different parameter types: + +@mcp_tool(name="file_processor", description="Process files with various options") +async def process_files( + file_paths: Annotated[list[str], "List of file paths to process"], + max_size: Annotated[int, "Maximum file size in bytes (default: 1MB)"] = 1024*1024, + format: Annotated[str, "Output format: 'json', 'csv', or 'txt'"] = "json", + recursive: Annotated[bool, "Whether to process subdirectories recursively"] = False +): + ''' + Example showing different parameter types with Annotated descriptions. + + Key points about Annotated: + - Works with any Python type: str, int, float, bool, list, dict, etc. + - The description should be clear and specific + - Can include examples, constraints, or default behavior + - Helps AI models understand how to use your tool correctly + ''' + pass +""" + +# ============================================================================= +# CHOOSING BETWEEN THE TWO APPROACHES +# ============================================================================= +""" +When to use Class-based approach: +- Complex tools with multiple related functions +- Tools that need initialization or configuration +- Tools that maintain state between calls +- Tools that need to share resources or connections +- When you want to group related functionality together + +When to use @mcp_tool decorator: +- Simple, stateless operations +- Quick prototyping and testing +- Single-purpose tools +- When you want minimal boilerplate code +- For functional programming style + +Both approaches support: +- Async/await operations +- Type annotations with Annotated for parameter descriptions +- Error handling and validation +- Return value serialization +- Integration with the MCP protocol +""" diff --git a/lib/serve/mcp-workbench/src/examples/sample_tools/text_utils.py b/lib/serve/mcp-workbench/src/examples/sample_tools/text_utils.py new file mode 100644 index 000000000..81871c978 --- /dev/null +++ b/lib/serve/mcp-workbench/src/examples/sample_tools/text_utils.py @@ -0,0 +1,75 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Example function-based MCP tools for text manipulation.""" + +from typing import Annotated + +# Note: In a real deployment, you would need to ensure mcpworkbench is available +# For development, you might need to adjust the import path or install the package +try: + from mcpworkbench.core.annotations import mcp_tool +except ImportError: + # Fallback for development - adjust path as needed + import os + import sys + + sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..")) + from mcpworkbench.core.annotations import mcp_tool + + +@mcp_tool( + name="text_length", + description="Count the number of characters in a text string", +) +async def count_characters(text: Annotated[str, "The text string to analyze"]): + """Count the number of characters in the given text.""" + return { + "text": text, + "character_count": len(text), + "word_count": len(text.split()), + "line_count": len(text.splitlines()), + } + + +@mcp_tool( + name="text_transform", + description="Transform text to uppercase, lowercase, or title case", +) +def transform_text( + text: Annotated[str, "The text string to transform"], + transformation: Annotated[str, "Type of transformation: 'upper', 'lower', 'title', or 'capitalize'"], +): + """Transform the given text according to the specified transformation.""" + if transformation == "upper": + result = text.upper() + elif transformation == "lower": + result = text.lower() + elif transformation == "title": + result = text.title() + elif transformation == "capitalize": + result = text.capitalize() + else: + raise ValueError(f"Unknown transformation: {transformation}") + + return {"original": text, "transformation": transformation, "result": result} + + +@mcp_tool( + name="text_reverse", + description="Reverse the characters in a text string", +) +def reverse_text(text: Annotated[str, "The text string to reverse"]): + """Reverse the characters in the given text.""" + return {"original": text, "reversed": text[::-1]} diff --git a/lib/serve/mcp-workbench/src/mcpworkbench/__init__.py b/lib/serve/mcp-workbench/src/mcpworkbench/__init__.py new file mode 100644 index 000000000..95f90f251 --- /dev/null +++ b/lib/serve/mcp-workbench/src/mcpworkbench/__init__.py @@ -0,0 +1,22 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +MCP Workbench - A dynamic host for Python files used as MCP tools. + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +Licensed under the Apache License, Version 2.0. +""" + +__version__ = "0.1.0" diff --git a/lib/serve/mcp-workbench/src/mcpworkbench/adapters/__init__.py b/lib/serve/mcp-workbench/src/mcpworkbench/adapters/__init__.py new file mode 100644 index 000000000..fa9fc09a8 --- /dev/null +++ b/lib/serve/mcp-workbench/src/mcpworkbench/adapters/__init__.py @@ -0,0 +1,19 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Adapters module for MCP Workbench tool integration.""" + +from .tool_adapter import BaseToolAdapter, create_adapter, FunctionToolAdapter, ToolAdapter + +__all__ = ["ToolAdapter", "BaseToolAdapter", "FunctionToolAdapter", "create_adapter"] diff --git a/lib/serve/mcp-workbench/src/mcpworkbench/adapters/tool_adapter.py b/lib/serve/mcp-workbench/src/mcpworkbench/adapters/tool_adapter.py new file mode 100644 index 000000000..b97b76a52 --- /dev/null +++ b/lib/serve/mcp-workbench/src/mcpworkbench/adapters/tool_adapter.py @@ -0,0 +1,122 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tool adapters for wrapping tools for MCP server integration.""" + +import asyncio +import logging +from abc import ABC, abstractmethod +from typing import Any, Dict + +from ..core.base_tool import BaseTool +from ..core.tool_discovery import ToolInfo, ToolType + +logger = logging.getLogger(__name__) + + +class ToolAdapter(ABC): + """Base class for tool adapters.""" + + def __init__(self, tool_info: ToolInfo): + self.tool_info = tool_info + + @abstractmethod + async def execute(self, arguments: Dict[str, Any]) -> Any: + """Execute the tool with the given arguments.""" + pass + + @property + def name(self) -> str: + """Get the tool name.""" + return self.tool_info.name + + @property + def description(self) -> str: + """Get the tool description.""" + return self.tool_info.description + + +class BaseToolAdapter(ToolAdapter): + """Adapter for BaseTool class instances.""" + + def __init__(self, tool_info: ToolInfo): + if tool_info.tool_type != ToolType.CLASS_BASED: + raise ValueError("BaseToolAdapter requires a class-based tool") + + if not isinstance(tool_info.tool_instance, BaseTool): + raise ValueError("Tool instance must be a BaseTool instance") + + super().__init__(tool_info) + self.tool_instance: BaseTool = tool_info.tool_instance + + async def execute(self, arguments: Dict[str, Any]) -> Any: + """Execute the BaseTool instance.""" + try: + # Call the tool's execute method + result = await self.tool_instance.execute(**arguments) + return result + except Exception as e: + logger.error(f"Error executing tool {self.name}: {e}") + raise + + +class FunctionToolAdapter(ToolAdapter): + """Adapter for @mcp_tool decorated functions.""" + + def __init__(self, tool_info: ToolInfo): + if tool_info.tool_type != ToolType.FUNCTION_BASED: + raise ValueError("FunctionToolAdapter requires a function-based tool") + + if not callable(tool_info.tool_instance): + raise ValueError("Tool instance must be callable") + + super().__init__(tool_info) + self.function = tool_info.tool_instance + + async def execute(self, arguments: Dict[str, Any]) -> Any: + """Execute the decorated function.""" + try: + # Check if the function is async + if asyncio.iscoroutinefunction(self.function): + result = await self.function(**arguments) + else: + # Run sync function in thread pool to avoid blocking + loop = asyncio.get_event_loop() + result = await loop.run_in_executor(None, lambda: self.function(**arguments)) + + return result + except Exception as e: + logger.error(f"Error executing function tool {self.name}: {e}") + raise + + +def create_adapter(tool_info: ToolInfo) -> ToolAdapter: + """ + Create the appropriate adapter for a tool. + + Args: + tool_info: Information about the tool to create an adapter for + + Returns: + A ToolAdapter instance for the given tool + + Raises: + ValueError: If the tool type is unknown or unsupported + """ + if tool_info.tool_type == ToolType.CLASS_BASED: + return BaseToolAdapter(tool_info) + elif tool_info.tool_type == ToolType.FUNCTION_BASED: + return FunctionToolAdapter(tool_info) + else: + raise ValueError(f"Unknown tool type: {tool_info.tool_type}") diff --git a/lib/serve/mcp-workbench/src/mcpworkbench/cli.py b/lib/serve/mcp-workbench/src/mcpworkbench/cli.py new file mode 100644 index 000000000..1bf7ab88a --- /dev/null +++ b/lib/serve/mcp-workbench/src/mcpworkbench/cli.py @@ -0,0 +1,176 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Command Line Interface for MCP Workbench.""" + +import logging +import re +import sys +from pathlib import Path +from typing import Optional + +import click +import yaml + +from .config.models import ServerConfig +from .core.tool_discovery import ToolDiscovery +from .core.tool_registry import ToolRegistry +from .server.mcp_server import MCPWorkbenchServer + +# Setup logging +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S" +) +logger = logging.getLogger(__name__) + + +def load_config_from_file(config_path: str) -> dict: + """Load configuration from YAML file.""" + try: + with open(config_path, "r") as f: + return yaml.safe_load(f) or {} + except FileNotFoundError: + logger.error(f"Configuration file not found: {config_path}") + sys.exit(1) + except yaml.YAMLError as e: + logger.error(f"Error parsing configuration file: {e}") + sys.exit(1) + except Exception as e: + logger.error(f"Error loading configuration file: {e}") + sys.exit(1) + + +def merge_config(file_config: dict, cli_overrides: dict) -> dict: + """Merge file configuration with CLI overrides.""" + merged = file_config.copy() + + # Apply CLI overrides + for key, value in cli_overrides.items(): + if value is not None: + merged[key] = value + + return merged + + +@click.command() +@click.option("--config", "-c", type=click.Path(exists=True, path_type=Path), help="Path to YAML configuration file") +@click.option( + "--tools-dir", + "-t", + type=click.Path(exists=True, file_okay=False, path_type=Path), + help="Directory containing tool files", +) +@click.option("--host", default=None, help="Server host address (default: 127.0.0.1)") +@click.option("--port", "-p", type=int, default=None, help="Server port (default: 8000)") +@click.option("--exit-route", default=None, help="Enable exit_server MCP tool (optional)") +@click.option("--rescan-route", default=None, help="Enable rescan_tools MCP tool (optional)") +@click.option("--cors-origins", default=None, help="Comma-separated list of allowed CORS origins (default: *)") +@click.option("--verbose", "-v", is_flag=True, help="Enable verbose logging") +@click.option("--debug", is_flag=True, help="Enable debug logging") +def main( + config: Optional[Path], + tools_dir: Optional[Path], + host: Optional[str], + port: Optional[int], + exit_route: Optional[str], + rescan_route: Optional[str], + cors_origins: Optional[str], + verbose: bool, + debug: bool, +): + """MCP Workbench - A dynamic host for Python files used as MCP tools.""" + + # Set logging level + if debug: + logging.getLogger().setLevel(logging.DEBUG) + elif verbose: + logging.getLogger().setLevel(logging.INFO) + + logger.info("Starting MCP Workbench...") + + # Load configuration + file_config = {} + if config: + logger.info(f"Loading configuration from {config}") + file_config = load_config_from_file(str(config)) + + # Prepare CLI overrides + cli_overrides = {} + + if tools_dir: + cli_overrides["tools_dir"] = str(tools_dir) + if host: + cli_overrides["host"] = host + if port: + cli_overrides["port"] = port + if exit_route: + cli_overrides["exit_route"] = exit_route + if rescan_route: + cli_overrides["rescan_route"] = rescan_route + + # Handle CORS origins + if cors_origins: + cleaned_origins = re.sub(r'^([\s"]+)?(.+?)([\s"]*)?$', r"\2", cors_origins) + origins = [origin.strip() for origin in cleaned_origins.split(",")] + cli_overrides["cors_origins"] = origins + + # Merge configurations + merged_config = merge_config(file_config, cli_overrides) + + # Validate required configuration + if "tools_dir" not in merged_config: + logger.error("Tools directory must be specified via --tools-dir or configuration file") + sys.exit(1) + + # Create server configuration + try: + server_config = ServerConfig.from_dict(merged_config) + except Exception as e: + logger.error(f"Invalid configuration: {e}") + sys.exit(1) + + logger.info("Configuration loaded:") + logger.info(f" Tools directory: {server_config.tools_directory}") + logger.info(f" Server: {server_config.server_host}:{server_config.server_port}") + logger.info(" Protocol: Pure MCP via FastMCP 2.0") + if server_config.exit_route_path: + logger.info(f" Exit tool enabled: {server_config.exit_route_path}") + if server_config.rescan_route_path: + logger.info(f" Rescan tool enabled: {server_config.rescan_route_path}") + + # Initialize components + try: + tool_discovery = ToolDiscovery(server_config.tools_directory) + tool_registry = ToolRegistry() + + # Create and start server + server = MCPWorkbenchServer(server_config, tool_discovery, tool_registry) + + logger.info("Server initialized successfully") + server.run() + + except KeyboardInterrupt: + logger.info("Received keyboard interrupt - shutting down") + sys.exit(0) + except Exception as e: + logger.error(f"Failed to start server: {e}") + if debug: + import traceback + + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/lib/serve/mcp-workbench/src/mcpworkbench/config/__init__.py b/lib/serve/mcp-workbench/src/mcpworkbench/config/__init__.py new file mode 100644 index 000000000..10f787a40 --- /dev/null +++ b/lib/serve/mcp-workbench/src/mcpworkbench/config/__init__.py @@ -0,0 +1,19 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Configuration module for MCP Workbench.""" + +from .models import CORSConfig, ServerConfig + +__all__ = ["ServerConfig", "CORSConfig"] diff --git a/lib/serve/mcp-workbench/src/mcpworkbench/config/models.py b/lib/serve/mcp-workbench/src/mcpworkbench/config/models.py new file mode 100644 index 000000000..51a209463 --- /dev/null +++ b/lib/serve/mcp-workbench/src/mcpworkbench/config/models.py @@ -0,0 +1,93 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Configuration models for MCP Workbench.""" + +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class CORSConfig(BaseModel): + """CORS configuration settings.""" + + allow_origins: List[str] = Field(default=["*"], description="Allowed origins for CORS") + allow_methods: List[str] = Field(default=["GET", "POST", "OPTIONS"], description="Allowed HTTP methods") + allow_headers: List[str] = Field(default=["*"], description="Allowed headers") + allow_credentials: bool = Field(default=True, description="Allow credentials in CORS requests") + expose_headers: List[str] = Field(default=[], description="Headers to expose to the browser") + max_age: int = Field(default=600, description="Maximum age for CORS preflight cache") + + +class ServerConfig(BaseModel): + """Main server configuration.""" + + # Server settings - using CLI-compatible names internally, but mapped from external names + server_host: str = Field(default="127.0.0.1", description="Server host address") + server_port: int = Field(default=8000, description="Server port") + + # Tool settings + tools_directory: str = Field(..., description="Directory containing tool files") + + # Management tool settings + exit_route_path: Optional[str] = Field(default=None, description="Enable exit_server MCP tool when set") + rescan_route_path: Optional[str] = Field(default=None, description="Enable rescan_tools MCP tool when set") + + # CORS settings + cors_settings: CORSConfig = Field(default_factory=CORSConfig, description="CORS configuration") + + @classmethod + def from_dict(cls, data: dict) -> "ServerConfig": + """Create ServerConfig from dictionary, handling both CLI and YAML formats.""" + # Create a copy to avoid modifying the original + config_data = data.copy() + + # Map CLI/YAML names to internal property names + field_mappings = { + # Server settings + "host": "server_host", + "port": "server_port", + # Tool settings + "tools_dir": "tools_directory", + # Route settings + "mcp_route": "mcp_route_path", + "exit_route": "exit_route_path", + "rescan_route": "rescan_route_path", + } + + # Apply field mappings + for yaml_key, internal_key in field_mappings.items(): + if yaml_key in config_data: + config_data[internal_key] = config_data.pop(yaml_key) + + # Handle CORS origins - support both simple list and full settings + if "cors_origins" in config_data: + cors_origins = config_data.pop("cors_origins") + + # If we don't have cors_settings yet, create one + if "cors_settings" not in config_data: + config_data["cors_settings"] = {} + + # If cors_settings is not a dict, make it one + if not isinstance(config_data["cors_settings"], dict): + config_data["cors_settings"] = {} + + # Set the origins + config_data["cors_settings"]["allow_origins"] = cors_origins + + # Handle cors_settings + if "cors_settings" in config_data and isinstance(config_data["cors_settings"], dict): + config_data["cors_settings"] = CORSConfig(**config_data["cors_settings"]) + + return cls(**config_data) diff --git a/lib/serve/mcp-workbench/src/mcpworkbench/core/__init__.py b/lib/serve/mcp-workbench/src/mcpworkbench/core/__init__.py new file mode 100644 index 000000000..1addf95bd --- /dev/null +++ b/lib/serve/mcp-workbench/src/mcpworkbench/core/__init__.py @@ -0,0 +1,22 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Core module for MCP Workbench tool management.""" + +from .annotations import mcp_tool +from .base_tool import BaseTool, ToolInfo +from .tool_discovery import RescanResult, ToolDiscovery +from .tool_registry import ToolRegistry + +__all__ = ["BaseTool", "ToolInfo", "mcp_tool", "ToolDiscovery", "RescanResult", "ToolRegistry"] diff --git a/lib/serve/mcp-workbench/src/mcpworkbench/core/annotations.py b/lib/serve/mcp-workbench/src/mcpworkbench/core/annotations.py new file mode 100644 index 000000000..c971b6f05 --- /dev/null +++ b/lib/serve/mcp-workbench/src/mcpworkbench/core/annotations.py @@ -0,0 +1,71 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Annotations for function-based MCP tools.""" + +from functools import wraps +from typing import Any, Callable, Dict + + +def mcp_tool(name: str, description: str): + """ + Decorator to mark a function as an MCP tool. + + Args: + name: The name of the tool + description: A description of what the tool does + + Returns: + The decorated function with MCP tool metadata + """ + + def decorator(func: Callable) -> Callable: + # Store metadata as function attributes + func._mcp_tool_name = name + func._mcp_tool_description = description + func._is_mcp_tool = True + + @wraps(func) + async def wrapper(*args, **kwargs): + # If the function is not already async, we need to handle it + if hasattr(func, "__code__") and func.__code__.co_flags & 0x80: # CO_COROUTINE + return await func(*args, **kwargs) + else: + return func(*args, **kwargs) + + # Copy metadata to wrapper + wrapper._mcp_tool_name = name + wrapper._mcp_tool_description = description + wrapper._is_mcp_tool = True + wrapper._original_func = func + + return wrapper + + return decorator + + +def is_mcp_tool(func: Callable) -> bool: + """Check if a function is marked as an MCP tool.""" + return hasattr(func, "_is_mcp_tool") and func._is_mcp_tool + + +def get_tool_metadata(func: Callable) -> Dict[str, Any]: + """Get the MCP tool metadata from a decorated function.""" + if not is_mcp_tool(func): + raise ValueError("Function is not marked as an MCP tool") + + return { + "name": getattr(func, "_mcp_tool_name", ""), + "description": getattr(func, "_mcp_tool_description", ""), + } diff --git a/lib/serve/mcp-workbench/src/mcpworkbench/core/base_tool.py b/lib/serve/mcp-workbench/src/mcpworkbench/core/base_tool.py new file mode 100644 index 000000000..4e9ad3d9a --- /dev/null +++ b/lib/serve/mcp-workbench/src/mcpworkbench/core/base_tool.py @@ -0,0 +1,86 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License."""Base tool class and related data structures.""" + +from abc import ABC, abstractmethod +from enum import Enum +from typing import Any, Callable, Optional, Union + +from pydantic import BaseModel, Field + + +class ToolType(str, Enum): + """Types of tools that can be discovered.""" + + CLASS_BASED = "class_based" + FUNCTION_BASED = "function_based" + + +class ToolInfo(BaseModel): + """Information about a discovered tool.""" + + name: str = Field(..., description="Tool name") + description: str = Field(..., description="Tool description") + tool_type: ToolType = Field(..., description="Type of tool (class or function based)") + file_path: str = Field(..., description="Path to the file containing the tool") + module_name: str = Field(..., description="Python module name") + + # For class-based tools + class_name: Optional[str] = Field(default=None, description="Class name for class-based tools") + + # For function-based tools + function_name: Optional[str] = Field(default=None, description="Function name for function-based tools") + + # Tool instance or function reference (not serialized) + tool_instance: Optional[Union[Any, Callable]] = Field( + default=None, exclude=True, description="Tool instance or function" + ) + + +class BaseTool(ABC): + """Abstract base class for MCP tools.""" + + def __init__(self, name: str, description: str): + """ + Initialize the tool with required metadata. + + Args: + name: The name of the tool + description: A description of what the tool does + """ + self.name = name + self.description = description + + @abstractmethod + async def execute(self) -> Callable[..., Any]: + """ + Returns an function to be executed as the tool. + + Returns: + The function to be executed + """ + pass diff --git a/lib/serve/mcp-workbench/src/mcpworkbench/core/tool_discovery.py b/lib/serve/mcp-workbench/src/mcpworkbench/core/tool_discovery.py new file mode 100644 index 000000000..01159e0df --- /dev/null +++ b/lib/serve/mcp-workbench/src/mcpworkbench/core/tool_discovery.py @@ -0,0 +1,274 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tool discovery component for MCP Workbench.""" + +import importlib +import importlib.util +import inspect +import logging +import sys +from pathlib import Path +from typing import Dict, List + +from pydantic import BaseModel + +from .annotations import get_tool_metadata, is_mcp_tool +from .base_tool import BaseTool, ToolInfo, ToolType + +logger = logging.getLogger(__name__) + + +class RescanResult(BaseModel): + """Result of a tool directory rescan.""" + + tools_added: List[str] = [] + tools_updated: List[str] = [] + tools_removed: List[str] = [] + total_tools: int = 0 + errors: List[str] = [] + + +class ToolDiscovery: + """Discovers and loads tools from Python files.""" + + def __init__(self, tools_directory: str): + """ + Initialize the tool discovery. + + Args: + tools_directory: Path to directory containing tool files + """ + self.tools_directory = Path(tools_directory) + self.loaded_modules: Dict[str, any] = {} + self.current_tools: Dict[str, ToolInfo] = {} + + if not self.tools_directory.exists(): + raise ValueError(f"Tools directory does not exist: {tools_directory}") + + if not self.tools_directory.is_dir(): + raise ValueError(f"Tools directory is not a directory: {tools_directory}") + + def discover_tools(self) -> List[ToolInfo]: + """ + Discover all tools in the tools directory. + + Returns: + List of discovered tool information + """ + tools = [] + + # Find all Python files in the directory + python_files = list(self.tools_directory.glob("*.py")) + + for file_path in python_files: + try: + file_tools = self._discover_tools_in_file(file_path) + tools.extend(file_tools) + except Exception as e: + logger.error(f"Error discovering tools in {file_path}: {e}") + continue + + # Update current tools tracking + self.current_tools = {tool.name: tool for tool in tools} + + return tools + + def rescan_tools(self) -> RescanResult: + """ + Rescan the tools directory and return changes. + + Returns: + RescanResult with information about changes + """ + result = RescanResult() + + # Store current tool names for comparison + old_tool_names = set(self.current_tools.keys()) + + # Reload all previously loaded modules to pick up changes + self._reload_modules() + + # Clear loaded modules tracking to force fresh discovery + self.loaded_modules.clear() + + # Discover tools fresh + try: + new_tools = self.discover_tools() + new_tool_names = {tool.name for tool in new_tools} + + # Calculate changes + result.tools_added = list(new_tool_names - old_tool_names) + result.tools_removed = list(old_tool_names - new_tool_names) + + # For tools that exist in both, check if they've been updated + # (This is a simple check - in practice you might want to compare timestamps or content hashes) + common_tools = new_tool_names & old_tool_names + result.tools_updated = list(common_tools) # Assume all common tools are updated for safety + + result.total_tools = len(new_tools) + + except Exception as e: + result.errors.append(f"Error during rescan: {str(e)}") + logger.error(f"Error during rescan: {e}") + + return result + + def _reload_modules(self): + """Reload all previously loaded modules to pick up file changes.""" + modules_to_reload = [] + + # Find all modules that match our naming pattern + for module_name in list(sys.modules.keys()): + if module_name.startswith("mcpworkbench_tools_"): + modules_to_reload.append(module_name) + + # Reload each module + for module_name in modules_to_reload: + try: + module = sys.modules[module_name] + importlib.reload(module) + logger.debug(f"Reloaded module: {module_name}") + except Exception as e: + logger.warning(f"Failed to reload module {module_name}: {e}") + # Remove the module from sys.modules if reload fails + # This will force a fresh load on next discovery + try: + del sys.modules[module_name] + except KeyError: + pass + + def _discover_tools_in_file(self, file_path: Path) -> List[ToolInfo]: + """ + Discover tools in a single Python file. + + Args: + file_path: Path to the Python file + + Returns: + List of tools found in the file + """ + tools = [] + + try: + # Create module name from file path + module_name = f"mcpworkbench_tools_{file_path.stem}" + + # Load the module + spec = importlib.util.spec_from_file_location(module_name, file_path) + if spec is None or spec.loader is None: + logger.warning(f"Could not create module spec for {file_path}") + return tools + + module = importlib.util.module_from_spec(spec) + + # Add to sys.modules to handle imports within the module + sys.modules[module_name] = module + + try: + spec.loader.exec_module(module) + self.loaded_modules[str(file_path)] = module + except Exception as e: + logger.error(f"Error executing module {file_path}: {e}") + return tools + + # Inspect the module for tools + tools.extend(self._find_class_based_tools(module, file_path, module_name)) + tools.extend(self._find_function_based_tools(module, file_path, module_name)) + + except Exception as e: + logger.error(f"Error loading module from {file_path}: {e}") + + return tools + + def _find_class_based_tools(self, module, file_path: Path, module_name: str) -> List[ToolInfo]: + """Find BaseTool subclasses in the module.""" + tools = [] + + for name, obj in inspect.getmembers(module, inspect.isclass): + # Skip imported classes (only look at classes defined in this module) + if obj.__module__ != module_name: + continue + + # Check if it's a subclass of BaseTool (but not BaseTool itself) + if issubclass(obj, BaseTool) and obj != BaseTool and not inspect.isabstract(obj): + + try: + # Try to instantiate the tool to get its metadata + # We need to handle different constructor signatures + sig = inspect.signature(obj.__init__) + params = list(sig.parameters.keys())[1:] # Skip 'self' + + if len(params) == 2 and "name" in params and "description" in params: + # Standard BaseTool signature - we need to provide name and description + # Try to get them from class attributes or use defaults + tool_name = getattr(obj, "name", name.lower()) + tool_description = getattr(obj, "description", f"Tool: {name}") + instance = obj(name=tool_name, description=tool_description) + else: + # Custom constructor - try to instantiate with no args + instance = obj() + + # Get tool metadata + tool_name = getattr(instance, "name", name.lower()) + tool_description = getattr(instance, "description", f"Tool: {name}") + + tool_info = ToolInfo( + name=tool_name, + description=tool_description, + tool_type=ToolType.CLASS_BASED, + file_path=str(file_path), + module_name=module_name, + class_name=name, + tool_instance=instance, + ) + + tools.append(tool_info) + + except Exception as e: + logger.error(f"Error instantiating tool class {name} from {file_path}: {e}") + continue + + return tools + + def _find_function_based_tools(self, module, file_path: Path, module_name: str) -> List[ToolInfo]: + """Find @mcp_tool decorated functions in the module.""" + tools = [] + + for name, obj in inspect.getmembers(module, inspect.isfunction): + # Skip imported functions (only look at functions defined in this module) + if obj.__module__ != module_name: + continue + + if is_mcp_tool(obj): + try: + metadata = get_tool_metadata(obj) + + tool_info = ToolInfo( + name=metadata["name"], + description=metadata["description"], + tool_type=ToolType.FUNCTION_BASED, + file_path=str(file_path), + module_name=module_name, + function_name=name, + tool_instance=obj, + ) + + tools.append(tool_info) + + except Exception as e: + logger.error(f"Error processing tool function {name} from {file_path}: {e}") + continue + + return tools diff --git a/lib/serve/mcp-workbench/src/mcpworkbench/core/tool_registry.py b/lib/serve/mcp-workbench/src/mcpworkbench/core/tool_registry.py new file mode 100644 index 000000000..4541cfb2f --- /dev/null +++ b/lib/serve/mcp-workbench/src/mcpworkbench/core/tool_registry.py @@ -0,0 +1,148 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tool registry component for MCP Workbench.""" + +import logging +import threading +from typing import Dict, List, Optional + +from .base_tool import ToolInfo + +logger = logging.getLogger(__name__) + + +class ToolRegistry: + """Thread-safe registry for managing discovered tools.""" + + def __init__(self): + """Initialize the tool registry.""" + self._tools: Dict[str, ToolInfo] = {} + self._lock = threading.RLock() + + def register_tool(self, tool_info: ToolInfo) -> None: + """ + Register a tool in the registry. + + Args: + tool_info: Information about the tool to register + """ + with self._lock: + self._tools[tool_info.name] = tool_info + logger.info(f"Registered tool: {tool_info.name}") + + def register_tools(self, tools: List[ToolInfo]) -> None: + """ + Register multiple tools in the registry. + + Args: + tools: List of tools to register + """ + with self._lock: + for tool in tools: + self._tools[tool.name] = tool + logger.info(f"Registered {len(tools)} tools") + + def unregister_tool(self, tool_name: str) -> bool: + """ + Unregister a tool from the registry. + + Args: + tool_name: Name of the tool to unregister + + Returns: + True if the tool was found and removed, False otherwise + """ + with self._lock: + if tool_name in self._tools: + del self._tools[tool_name] + logger.info(f"Unregistered tool: {tool_name}") + return True + return False + + def get_tool(self, tool_name: str) -> Optional[ToolInfo]: + """ + Get a tool by name. + + Args: + tool_name: Name of the tool to retrieve + + Returns: + ToolInfo if found, None otherwise + """ + with self._lock: + return self._tools.get(tool_name) + + def list_tools(self) -> List[ToolInfo]: + """ + Get a list of all registered tools. + + Returns: + List of all registered tools + """ + with self._lock: + return list(self._tools.values()) + + def list_tool_names(self) -> List[str]: + """ + Get a list of all registered tool names. + + Returns: + List of all registered tool names + """ + with self._lock: + return list(self._tools.keys()) + + def clear(self) -> None: + """Clear all tools from the registry.""" + with self._lock: + self._tools.clear() + logger.info("Cleared all tools from registry") + + def update_registry(self, new_tools: List[ToolInfo]) -> None: + """ + Update the registry with a new set of tools. + This replaces all existing tools. + + Args: + new_tools: New list of tools to register + """ + with self._lock: + self._tools.clear() + for tool in new_tools: + self._tools[tool.name] = tool + logger.info(f"Updated registry with {len(new_tools)} tools") + + def get_tool_count(self) -> int: + """ + Get the number of registered tools. + + Returns: + Number of registered tools + """ + with self._lock: + return len(self._tools) + + def has_tool(self, tool_name: str) -> bool: + """ + Check if a tool is registered. + + Args: + tool_name: Name of the tool to check + + Returns: + True if the tool is registered, False otherwise + """ + with self._lock: + return tool_name in self._tools diff --git a/lib/serve/mcp-workbench/src/mcpworkbench/server/__init__.py b/lib/serve/mcp-workbench/src/mcpworkbench/server/__init__.py new file mode 100644 index 000000000..10400d77a --- /dev/null +++ b/lib/serve/mcp-workbench/src/mcpworkbench/server/__init__.py @@ -0,0 +1,20 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Server module for MCP Workbench HTTP server.""" + +from .mcp_server import MCPWorkbenchServer +from .middleware import CORSMiddleware, ExitRouteMiddleware, RescanMiddleware + +__all__ = ["MCPWorkbenchServer", "CORSMiddleware", "ExitRouteMiddleware", "RescanMiddleware"] diff --git a/lib/serve/mcp-workbench/src/mcpworkbench/server/auth.py b/lib/serve/mcp-workbench/src/mcpworkbench/server/auth.py new file mode 100644 index 000000000..e648c5bcf --- /dev/null +++ b/lib/serve/mcp-workbench/src/mcpworkbench/server/auth.py @@ -0,0 +1,258 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Authentication for FastAPI app.""" +import os +import ssl +import sys +from datetime import datetime +from pathlib import Path +from time import time +from typing import Any, Dict, Optional + +import boto3 +import jwt +import requests +from fastapi import Request +from loguru import logger +from starlette.middleware.base import BaseHTTPMiddleware, DispatchFunction +from starlette.responses import JSONResponse, Response +from starlette.types import ASGIApp + +# The following are field names, not passwords or tokens +API_KEY_HEADER_NAMES = [ + "authorization", + "Authorization", # OpenAI Bearer token format, collides with IdP, but that's okay for this use case + "Api-Key", # pragma: allowlist secret # Azure key format, can be used with Continue IDE plugin +] +TOKEN_EXPIRATION_NAME = "tokenExpiration" # nosec B105 +TOKEN_TABLE_NAME = "TOKEN_TABLE_NAME" # nosec B105 +USE_AUTH = "USE_AUTH" + + +logger_level = os.environ.get("LOG_LEVEL", "INFO") +logger.configure( + handlers=[ + { + "sink": sys.stdout, + "format": ("{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}"), + "level": logger_level.upper(), + } + ] +) + + +def is_idp_used() -> bool: + """Get if the identity provider is being used based on environment variable.""" + return os.environ.get(USE_AUTH, "false").lower() == "true" + + +if not jwt.algorithms.has_crypto: + logger.error("No crypto support for JWT.") + raise RuntimeError("No crypto support for JWT.") + + +def get_oidc_metadata(cert_path: Optional[str] = None) -> Dict[str, Any]: + """Get OIDC endpoints and metadata from authority.""" + authority = os.environ.get("AUTHORITY") + resp = requests.get(f"{authority}/.well-known/openid-configuration", verify=cert_path or True, timeout=30) + resp.raise_for_status() + return resp.json() # type: ignore + + +def get_jwks_client() -> jwt.PyJWKClient: + """Get JWK Client for JWT signing operations.""" + if "SSL_CERT_FILE" not in os.environ: + cert_path = None + logger.info("Using default certificate for SSL verification.") + else: + cert_path = str(Path(os.environ["SSL_CERT_FILE"]).absolute()) + logger.info("Using self-signed certificate for SSL verification.") + ssl_context = ssl.create_default_context() + if cert_path: + ssl_context.load_verify_locations(cert_path) + oidc_metadata = get_oidc_metadata(cert_path) + return jwt.PyJWKClient(oidc_metadata["jwks_uri"], cache_jwk_set=True, lifespan=360, ssl_context=ssl_context) + + +def id_token_is_valid( + id_token: str, client_id: str, authority: str, jwks_client: jwt.PyJWKClient +) -> Optional[Dict[str, Any]]: + """Check whether an ID token is valid and return decoded data.""" + try: + signing_key = jwks_client.get_signing_key_from_jwt(id_token) + data: Dict[str, Any] = jwt.decode( + id_token, + signing_key.key, + algorithms=["RS256"], + issuer=authority, + audience=client_id, + options={ + "verify_signature": True, + "verify_exp": True, + "verify_nbf": True, + "verify_iat": True, + "verify_aud": True, + "verify_iss": True, + }, + ) + return data + except jwt.exceptions.PyJWTError as e: + logger.exception(e) + return None + + +def is_user_in_group(jwt_data: dict[str, Any], group: str, jwt_groups_property: str) -> bool: + """Check if the user is an admin.""" + props = jwt_groups_property.split(".") + current_node = jwt_data + for prop in props: + if prop in current_node: + current_node = current_node[prop] + else: + return False + return group in current_node + + +def get_authorization_token(headers: Dict[str, str], header_name: str) -> str: + """Get Bearer token from Authorization headers if it exists.""" + if header_name in headers: + return headers.get(header_name, "").removeprefix("Bearer").strip() + return headers.get(header_name.lower(), "").removeprefix("Bearer").strip() + + +class LoggingMiddleware(BaseHTTPMiddleware): + def __init__(self, app: ASGIApp, dispatch: DispatchFunction | None = None): + super().__init__(app, dispatch) + + async def dispatch(self, request, call_next): + response = await call_next(request) + response.headers["Custom"] = "Example" + return response + + +class OIDCHTTPBearer(BaseHTTPMiddleware): + """OIDC based bearer token authenticator.""" + + def __init__(self, app: ASGIApp, dispatch: DispatchFunction | None = None): + super().__init__(app, dispatch) + self._token_authorizer = ApiTokenAuthorizer() + self._management_token_authorizer = ManagementTokenAuthorizer() + self._jwks_client = get_jwks_client() + + async def dispatch(self, request: Request, call_next) -> Response: + """Verify the provided bearer token or API Key. API Key will take precedence over the bearer token.""" + if request.method == "OPTIONS": + return await call_next(request) + + valid = False + if self._token_authorizer.is_valid_api_token(request.headers): + logger.info("looks like a valid api token") + valid = True + elif self._management_token_authorizer.is_valid_api_token(request.headers): + logger.info("looks like a valid mgmt token") + valid = True + else: + for header_name in API_KEY_HEADER_NAMES: + authorization = request.headers.get(header_name, "").strip() + id_token = authorization.split(" ")[-1] + if len(id_token) > 0 and id_token_is_valid( + id_token=id_token, + authority=os.environ["AUTHORITY"], + client_id=os.environ["CLIENT_ID"], + jwks_client=self._jwks_client, + ): + valid = True + break + + if not valid: + # raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Not authenticated") + return JSONResponse( + status_code=401, + content={"detail": "Unauthorized"}, + ) + + return await call_next(request) + + +class ApiTokenAuthorizer: + """Class for checking API tokens against a DynamoDB table of API Tokens. + + For the Token database, only a string value in the "token" field is required. Optionally, + customers may put a UNIX timestamp (in seconds) in a "tokenExpiration" field so that the + API key becomes invalid after a specified time. + """ + + def __init__(self) -> None: + ddb_resource = boto3.resource("dynamodb", region_name=os.environ["AWS_REGION"]) + self._token_table = ddb_resource.Table(os.environ[TOKEN_TABLE_NAME]) + + def _get_token_info(self, token: str) -> Any: + """Return DDB entry for token if it exists.""" + ddb_response = self._token_table.get_item(Key={"token": token}, ReturnConsumedCapacity="NONE") + return ddb_response.get("Item", None) + + def is_valid_api_token(self, headers: Dict[str, str]) -> bool: + """Return if API Token from request headers is valid if found.""" + for header_name in API_KEY_HEADER_NAMES: + token = headers.get(header_name, "").removeprefix("Bearer").strip() + if len(token) > 0: + token_info = self._get_token_info(token) + if token_info: + token_expiration = int(token_info.get(TOKEN_EXPIRATION_NAME, datetime.max.timestamp())) + current_time = int(datetime.now().timestamp()) + if current_time < token_expiration: # token has not expired yet + return True + return False + + +class ManagementTokenAuthorizer: + """Class for checking Management tokens against a SecretsManager secret.""" + + def __init__(self) -> None: + self._secrets_manager = boto3.client("secretsmanager", region_name=os.environ["AWS_REGION"]) + self._secret_tokens: list[str] = [] + self._last_run = 0 + + def _refreshTokens(self) -> None: + """Refresh secret management tokens.""" + current_time = int(time()) + if current_time - (self._last_run or 0) > 3600: + secret_tokens = [] + secret_tokens.append( + self._secrets_manager.get_secret_value( + SecretId=os.environ.get("MANAGEMENT_KEY_NAME"), VersionStage="AWSCURRENT" + )["SecretString"] + ) + try: + secret_tokens.append( + self._secrets_manager.get_secret_value( + SecretId=os.environ.get("MANAGEMENT_KEY_NAME"), VersionStage="AWSPREVIOUS" + )["SecretString"] + ) + except Exception: + logger.info(f"No previous secret version for {os.environ.get('MANAGEMENT_KEY_NAME')}") + self._secret_tokens = secret_tokens + self._last_run = current_time + + def is_valid_api_token(self, headers: Dict[str, str]) -> bool: + """Return if API Token from request headers is valid if found.""" + self._refreshTokens() + + for header_name in API_KEY_HEADER_NAMES: + token = headers.get(header_name, "").strip() + if token in self._secret_tokens: + return True + + return False diff --git a/lib/serve/mcp-workbench/src/mcpworkbench/server/mcp_server.py b/lib/serve/mcp-workbench/src/mcpworkbench/server/mcp_server.py new file mode 100644 index 000000000..3efc6e5b1 --- /dev/null +++ b/lib/serve/mcp-workbench/src/mcpworkbench/server/mcp_server.py @@ -0,0 +1,277 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""MCP Workbench FastMCP 2.0 server implementation.""" + +import asyncio +import inspect +import logging +import sys +from datetime import datetime +from typing import Any, Dict, List + +from fastmcp import FastMCP +from starlette.applications import Starlette +from starlette.middleware.cors import CORSMiddleware +from starlette.responses import JSONResponse +from starlette.routing import Mount, Route + +from ..config.models import ServerConfig +from ..core.base_tool import BaseTool +from ..core.tool_discovery import ToolDiscovery, ToolInfo, ToolType +from ..core.tool_registry import ToolRegistry +from .auth import OIDCHTTPBearer + +logger = logging.getLogger(__name__) + + +class MCPWorkbenchServer: + """MCP Workbench server using pure FastMCP 2.0.""" + + def __init__(self, config: ServerConfig, tool_discovery: ToolDiscovery, tool_registry: ToolRegistry): + """ + Initialize the MCP Workbench server. + + Args: + config: Server configuration + tool_discovery: Tool discovery instance + tool_registry: Tool registry instance + """ + self.config = config + self.tool_discovery = tool_discovery + self.tool_registry = tool_registry + self.registered_tools: Dict[str, Any] = {} + + # Create FastMCP application + self.app = FastMCP("mcpworkbench") + logger.info("FastMCP 2.0 server initialized") + + # Register built-in management tools + self._register_management_tools() + + def _register_management_tools(self): + """Register built-in management tools - now removed as they are HTTP routes.""" + # Management functionality moved to HTTP GET endpoints + pass + + def _add_management_routes(self, app: Starlette): + if self.config.exit_route_path: + + async def exit_endpoint(request): + """HTTP GET endpoint to gracefully shutdown the server.""" + logger.info("Exit requested via HTTP endpoint") + + # Schedule shutdown after response is sent + async def delayed_shutdown(): + await asyncio.sleep(1) + logger.info("Shutting down server...") + sys.exit(0) + + asyncio.create_task(delayed_shutdown()) + + result = { + "status": "success", + "message": "Server shutdown initiated", + "timestamp": datetime.utcnow().isoformat() + "Z", + } + return JSONResponse(result) + + app.add_route(self.config.exit_route_path, exit_endpoint, methods=["GET"]) + + if self.config.rescan_route_path: + + async def rescan_endpoint(request): + """HTTP GET endpoint to rescan tools directory and reload tools.""" + try: + logger.info("Rescanning tools directory via HTTP...") + + # Use the enhanced rescan_tools method which includes module reloading + rescan_result = self.tool_discovery.rescan_tools() + + # Clear existing registered tools tracking + # Note: FastMCP 2.0 may not have tool unregistration + # This is a limitation we'll document + self.registered_tools.clear() + + # Get the newly discovered tools and register them + tools = list(self.tool_discovery.current_tools.values()) + self.tool_registry.register_tools(tools) + await self._register_discovered_tools(tools) + + # Build result using the rescan_result data + result = { + "status": "success", + "tools_added": rescan_result.tools_added, + "tools_updated": rescan_result.tools_updated, + "tools_removed": rescan_result.tools_removed, + "total_tools": rescan_result.total_tools, + "errors": rescan_result.errors, + "timestamp": datetime.utcnow().isoformat() + "Z", + } + + logger.info(f"Rescan completed: {result}") + return JSONResponse(result) + + except Exception as e: + logger.error(f"Error during rescan: {e}") + error_result = { + "status": "error", + "error": str(e), + "timestamp": datetime.utcnow().isoformat() + "Z", + } + return JSONResponse(error_result, status_code=500) + + app.add_route(self.config.rescan_route_path, rescan_endpoint, methods=["GET"]) + + def _create_starlette_app(self): + """Create Starlette application with MCP and HTTP routes.""" + + mcp_app = self.app.http_app(path="/", transport="streamable-http", stateless_http=True) + + async def health_check(request): + """Health check endpoint for Docker health checks.""" + return JSONResponse({"status": "healthy", "service": "mcpworkbench"}) + + logger.info(f"CORS Allowed Origins: {self.config.cors_settings.allow_origins}") + mcp_app.add_middleware( + CORSMiddleware, + allow_origins=self.config.cors_settings.allow_origins, + allow_methods=self.config.cors_settings.allow_methods, + allow_headers=self.config.cors_settings.allow_headers, + ) + + mcp_app.add_middleware(OIDCHTTPBearer) + + # Add MCP mount + routes = [ + Route("/health", health_check), + Mount("/v2/mcp", mcp_app), + ] + + self._add_management_routes(mcp_app) + + return Starlette(routes=routes, lifespan=mcp_app.lifespan) + + async def _register_discovered_tools(self, tools: List[ToolInfo]): + """Register discovered tools with FastMCP.""" + for tool_info in tools: + try: + await self._register_single_tool(tool_info) + except Exception as e: + logger.error(f"Failed to register tool {tool_info.name}: {e}") + + async def _register_single_tool(self, tool_info: ToolInfo): + """Register a single discovered tool with FastMCP.""" + if tool_info.tool_type == ToolType.CLASS_BASED: + await self._register_class_tool(tool_info) + elif tool_info.tool_type == ToolType.FUNCTION_BASED: + await self._register_function_tool(tool_info) + else: + logger.error(f"Unknown tool type for {tool_info.name}: {tool_info.tool_type}") + + async def _register_class_tool(self, tool_info: ToolInfo): + """Register a class-based tool with FastMCP.""" + if not isinstance(tool_info.tool_instance, BaseTool): + raise ValueError(f"Class tool {tool_info.name} instance must be a BaseTool") + + tool_instance = tool_info.tool_instance + + tool = await tool_instance.execute() + + # Register with FastMCP using the tool's metadata + self.app.tool( + name=tool_info.name, + description=tool_info.description, + )(tool) + + self.registered_tools[tool_info.name] = tool_info + logger.debug(f"Registered class-based tool: {tool_info.name}") + + async def _register_function_tool(self, tool_info: ToolInfo): + """Register a function-based tool with FastMCP.""" + if not callable(tool_info.tool_instance): + raise ValueError(f"Function tool {tool_info.name} instance must be callable") + + function = tool_info.tool_instance + + # Check if function is async + is_async = inspect.iscoroutinefunction(function) + + if is_async: + # Function is already async + wrapper_func = function + else: + # Wrap sync function to be async + async def async_wrapper(**kwargs): + return function(**kwargs) + + wrapper_func = async_wrapper + + # Register with FastMCP using the tool's metadata + self.app.tool( + name=tool_info.name, + description=tool_info.description, + )(wrapper_func) + + self.registered_tools[tool_info.name] = tool_info + logger.debug(f"Registered function-based tool: {tool_info.name}") + + async def discover_and_register_tools(self): + """Discover and register initial tools.""" + logger.info("Discovering initial tools...") + tools = self.tool_discovery.discover_tools() + self.tool_registry.register_tools(tools) + await self._register_discovered_tools(tools) + + logger.info(f"Registered {len(tools)} tools") + for tool in tools: + logger.info(f" - {tool.name}: {tool.description}") + + return tools + + async def start(self): + """Start the server.""" + # Discover and register tools + await self.discover_and_register_tools() + + # Create Starlette app with both MCP and HTTP routes + starlette_app = self._create_starlette_app() + + # Start server with Starlette app + logger.info(f"Starting MCP Workbench server on {self.config.server_host}:{self.config.server_port}") + logger.info("Available endpoints:") + logger.info(" - MCP Protocol: /v2/mcp") + + if self.config.rescan_route_path: + logger.info(f" - Rescan Tools: GET {self.config.rescan_route_path}") + if self.config.exit_route_path: + logger.info(f" - Exit Server: GET {self.config.exit_route_path}") + + # Use uvicorn to serve the Starlette app + import uvicorn + + config = uvicorn.Config( + starlette_app, + host=self.config.server_host, + port=self.config.server_port, + log_level="info", + forwarded_allow_ips="*", + ) + server = uvicorn.Server(config) + await server.serve() + + def run(self): + """Run the server (blocking).""" + # Use a more robust approach to handle event loops + asyncio.run(self.start()) diff --git a/lib/serve/mcp-workbench/src/mcpworkbench/server/middleware.py b/lib/serve/mcp-workbench/src/mcpworkbench/server/middleware.py new file mode 100644 index 000000000..d165b59eb --- /dev/null +++ b/lib/serve/mcp-workbench/src/mcpworkbench/server/middleware.py @@ -0,0 +1,140 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Middleware components for MCP Workbench server.""" + +import logging +import sys +from datetime import datetime +from typing import Callable + +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.middleware.cors import CORSMiddleware as StarletteCORSMiddleware +from starlette.requests import Request +from starlette.responses import JSONResponse, Response + +from ..config.models import CORSConfig +from ..core.tool_discovery import ToolDiscovery +from ..core.tool_registry import ToolRegistry + +logger = logging.getLogger(__name__) + + +class CORSMiddleware(StarletteCORSMiddleware): + """CORS middleware wrapper for configuration compatibility.""" + + def __init__(self, app, cors_config: CORSConfig): + super().__init__( + app, + allow_origins=cors_config.allow_origins, + allow_methods=cors_config.allow_methods, + allow_headers=cors_config.allow_headers, + allow_credentials=cors_config.allow_credentials, + expose_headers=cors_config.expose_headers, + max_age=cors_config.max_age, + ) + + +class ExitRouteMiddleware(BaseHTTPMiddleware): + """Middleware to handle application exit requests.""" + + def __init__(self, app, exit_path: str): + super().__init__(app) + self.exit_path = exit_path.rstrip("/") + + async def dispatch(self, request: Request, call_next: Callable) -> Response: + """Handle the request and check for exit route.""" + # Normalize the request path + request_path = request.url.path.rstrip("/") + + if request_path == self.exit_path: + logger.info("Exit route called - shutting down server") + + # Return success response before exiting + response = JSONResponse( + {"status": "ok", "message": "Server shutting down", "timestamp": datetime.now().isoformat()} + ) + + # Schedule the exit to happen after response is sent + import asyncio + + asyncio.create_task(self._delayed_exit()) + + return response + + # Continue with normal request processing + return await call_next(request) + + async def _delayed_exit(self): + """Exit the application after a short delay.""" + import asyncio + + await asyncio.sleep(0.1) # Short delay to ensure response is sent + logger.info("Exiting application") + sys.exit(0) + + +class RescanMiddleware(BaseHTTPMiddleware): + """Middleware to handle tool rescanning requests.""" + + def __init__(self, app, rescan_path: str, tool_discovery: ToolDiscovery, tool_registry: ToolRegistry): + super().__init__(app) + self.rescan_path = rescan_path.rstrip("/") + self.tool_discovery = tool_discovery + self.tool_registry = tool_registry + + async def dispatch(self, request: Request, call_next: Callable) -> Response: + """Handle the request and check for rescan route.""" + # Normalize the request path + request_path = request.url.path.rstrip("/") + + if request_path == self.rescan_path: + logger.info("Rescan route called - rescanning tools") + + try: + # Perform the rescan + rescan_result = self.tool_discovery.rescan_tools() + + # Update the registry with new tools + new_tools = self.tool_discovery.discover_tools() + self.tool_registry.update_registry(new_tools) + + # Return rescan results + response_data = { + "status": "success", + "tools_added": rescan_result.tools_added, + "tools_updated": rescan_result.tools_updated, + "tools_removed": rescan_result.tools_removed, + "total_tools": rescan_result.total_tools, + "errors": rescan_result.errors, + "timestamp": datetime.now().isoformat(), + } + + logger.info( + f"Rescan completed: {len(rescan_result.tools_added)} added, " + f"{len(rescan_result.tools_updated)} updated, " + f"{len(rescan_result.tools_removed)} removed" + ) + + return JSONResponse(response_data) + + except Exception as e: + logger.error(f"Error during rescan: {e}") + return JSONResponse( + {"status": "error", "message": f"Rescan failed: {str(e)}", "timestamp": datetime.now().isoformat()}, + status_code=500, + ) + + # Continue with normal request processing + return await call_next(request) diff --git a/lib/serve/mcp-workbench/test_install.py b/lib/serve/mcp-workbench/test_install.py new file mode 100644 index 000000000..dad143ade --- /dev/null +++ b/lib/serve/mcp-workbench/test_install.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Simple test to verify MCP Workbench installation. +Run this after installing to verify everything works. +""" + + +def test_cli_available(): + """Test that the CLI command is available.""" + import subprocess + import sys + + try: + result = subprocess.run( + [sys.executable, "-m", "mcpworkbench.cli", "--help"], capture_output=True, text=True, timeout=10 + ) + if result.returncode == 0 and "mcpworkbench" in result.stdout.lower(): + print("✅ CLI command is available!") + return True + else: + print(f"❌ CLI test failed. Return code: {result.returncode}") + print(f"STDOUT: {result.stdout}") + print(f"STDERR: {result.stderr}") + return False + except Exception as e: + print(f"❌ CLI test failed: {e}") + return False + + +def test_basic_functionality(): + """Test basic functionality works.""" + try: + # Create a simple tool class + from mcpworkbench.core.base_tool import BaseTool + + class TestTool(BaseTool): + def __init__(self): + super().__init__("test", "A test tool") + + async def execute(self, **kwargs): + return {"result": "test successful"} + + # Test tool instantiation + tool = TestTool() + assert tool.name == "test" + assert tool.description == "A test tool" + + # Test annotation + from mcpworkbench.core.annotations import mcp_tool + + @mcp_tool(name="test_func", description="Test function") + def test_func(): + return "annotated test successful" + + assert hasattr(test_func, "_is_mcp_tool") + + print("✅ Basic functionality test passed!") + return True + + except Exception as e: + print(f"❌ Basic functionality test failed: {e}") + return False + + +def main(): + """Run all installation tests.""" + print("Testing MCP Workbench installation...") + print("=" * 50) + + tests = [ + ("CLI Test", test_cli_available), + ("Basic Functionality Test", test_basic_functionality), + ] + + passed = 0 + total = len(tests) + + for test_name, test_func in tests: + print(f"\nRunning {test_name}...") + if test_func(): + passed += 1 + else: + print(f"❌ {test_name} failed") + + print("\n" + "=" * 50) + print(f"Results: {passed}/{total} tests passed") + + if passed == total: + print("🎉 Installation verification successful!") + print("\nYou can now use MCP Workbench:") + print(" mcpworkbench --help") + print(" python -m mcpworkbench.cli --help") + else: + print("❌ Installation verification failed!") + print("\nTry reinstalling:") + print(" pip install -e .") + print(" # or") + print(' pip install -e ".[dev]"') + + return passed == total + + +if __name__ == "__main__": + import sys + + sys.exit(0 if main() else 1) diff --git a/lib/serve/mcp-workbench/tests/__init__.py b/lib/serve/mcp-workbench/tests/__init__.py new file mode 100644 index 000000000..9c6de76ba --- /dev/null +++ b/lib/serve/mcp-workbench/tests/__init__.py @@ -0,0 +1,15 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for MCP Workbench.""" diff --git a/lib/serve/mcp-workbench/tests/conftest.py b/lib/serve/mcp-workbench/tests/conftest.py new file mode 100644 index 000000000..3d8de2cfd --- /dev/null +++ b/lib/serve/mcp-workbench/tests/conftest.py @@ -0,0 +1,163 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Pytest configuration and shared fixtures.""" + +import asyncio +import tempfile +from pathlib import Path +from typing import Generator + +import pytest +from mcpworkbench.config.models import CORSConfig, ServerConfig +from mcpworkbench.core.tool_discovery import ToolDiscovery +from mcpworkbench.core.tool_registry import ToolRegistry + + +@pytest.fixture +def temp_tools_dir() -> Generator[Path, None, None]: + """Create a temporary directory for test tools.""" + with tempfile.TemporaryDirectory() as temp_dir: + tools_dir = Path(temp_dir) + yield tools_dir + + +@pytest.fixture +def sample_function_tool_content() -> str: + """Sample function-based tool content.""" + return """ +from mcpworkbench.core.annotations import mcp_tool + +@mcp_tool( + name="echo_test", + description="Echo back the input text for testing", + parameters={ + "type": "object", + "properties": { + "message": {"type": "string", "description": "Message to echo"} + }, + "required": ["message"] + } +) +def echo_message(message: str): + return {"echoed": message, "length": len(message)} + +@mcp_tool( + name="add_test", + description="Add two numbers together for testing", + parameters={ + "type": "object", + "properties": { + "a": {"type": "number", "description": "First number"}, + "b": {"type": "number", "description": "Second number"} + }, + "required": ["a", "b"] + } +) +async def add_numbers(a: float, b: float): + return {"a": a, "b": b, "sum": a + b} +""" + + +@pytest.fixture +def sample_class_tool_content() -> str: + """Sample class-based tool content.""" + return ''' +from mcpworkbench.core.base_tool import BaseTool + +class TestGreetingTool(BaseTool): + """Test greeting tool.""" + + def __init__(self): + super().__init__( + name="greeting_test", + description="Generate personalized greetings for testing" + ) + + def get_parameters(self): + return { + "type": "object", + "properties": { + "name": {"type": "string", "description": "Name to greet"}, + "style": { + "type": "string", + "enum": ["formal", "casual", "enthusiastic"], + "description": "Greeting style", + "default": "casual" + } + }, + "required": ["name"] + } + + async def execute(self, **kwargs): + name = kwargs["name"] + style = kwargs.get("style", "casual") + + if style == "formal": + greeting = f"Good day, {name}." + elif style == "enthusiastic": + greeting = f"Hey there, {name}! How exciting to meet you!" + else: # casual + greeting = f"Hi {name}!" + + return {"greeting": greeting, "name": name, "style": style} +''' + + +@pytest.fixture +def populated_tools_dir( + temp_tools_dir: Path, sample_function_tool_content: str, sample_class_tool_content: str +) -> Path: + """Create a tools directory populated with sample tools.""" + # Create function-based tools file + (temp_tools_dir / "function_tools.py").write_text(sample_function_tool_content) + + # Create class-based tools file + (temp_tools_dir / "class_tools.py").write_text(sample_class_tool_content) + + return temp_tools_dir + + +@pytest.fixture +def sample_config() -> ServerConfig: + """Sample server configuration for testing.""" + return ServerConfig( + server_host="127.0.0.1", + server_port=8001, + tools_directory="/tmp/test-tools", + mcp_route_path="/mcp", + exit_route_path="/shutdown", + rescan_route_path="/rescan", + cors_settings=CORSConfig(allow_origins=["*"], allow_methods=["GET", "POST"], allow_headers=["*"]), + ) + + +@pytest.fixture +def tool_discovery(populated_tools_dir: Path) -> ToolDiscovery: + """Create a tool discovery instance with populated tools directory.""" + return ToolDiscovery(str(populated_tools_dir)) + + +@pytest.fixture +def tool_registry() -> ToolRegistry: + """Create a fresh tool registry instance.""" + return ToolRegistry() + + +@pytest.fixture +def event_loop(): + """Create an instance of the default event loop for the test session.""" + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() diff --git a/lib/serve/mcp-workbench/tests/test_adapters.py b/lib/serve/mcp-workbench/tests/test_adapters.py new file mode 100644 index 000000000..b2e35ff27 --- /dev/null +++ b/lib/serve/mcp-workbench/tests/test_adapters.py @@ -0,0 +1,284 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for tool adapters.""" + +import pytest +from mcpworkbench.adapters.tool_adapter import BaseToolAdapter, create_adapter, FunctionToolAdapter +from mcpworkbench.core.annotations import mcp_tool +from mcpworkbench.core.base_tool import BaseTool, ToolInfo, ToolType + + +class MockBaseTool(BaseTool): + """Mock implementation of BaseTool for testing.""" + + def __init__(self): + super().__init__("test_base_tool", "A test base tool") + + async def execute(self, **kwargs): + message = kwargs.get("message", "default") + return {"processed": f"Base tool processed: {message}"} + + def get_parameters(self): + return {"type": "object", "properties": {"message": {"type": "string", "description": "Message to process"}}} + + +@mcp_tool( + name="test_function_tool", + description="A test function tool", + parameters={ + "type": "object", + "properties": {"value": {"type": "number", "description": "Value to process"}}, + "required": ["value"], + }, +) +def mock_function(value: float): + """Mock function for function adapter testing.""" + return {"doubled": value * 2} + + +@mcp_tool(name="test_async_function_tool", description="A test async function tool") +async def mock_async_function(text: str): + """Mock async function for function adapter testing.""" + return {"uppercased": text.upper()} + + +class TestBaseToolAdapter: + """Test BaseToolAdapter functionality.""" + + def test_adapter_creation(self): + """Test creating a BaseToolAdapter.""" + tool_instance = MockBaseTool() + tool_info = ToolInfo( + name="test_base_tool", + description="A test base tool", + tool_type=ToolType.CLASS_BASED, + file_path="/test/path.py", + module_name="test_module", + class_name="MockBaseTool", + tool_instance=tool_instance, + ) + + adapter = BaseToolAdapter(tool_info) + assert adapter.name == "test_base_tool" + assert adapter.description == "A test base tool" + assert adapter.tool_instance == tool_instance + + def test_adapter_wrong_type(self): + """Test BaseToolAdapter with wrong tool type.""" + tool_info = ToolInfo( + name="test_tool", + description="A test tool", + tool_type=ToolType.FUNCTION_BASED, # Wrong type + file_path="/test/path.py", + module_name="test_module", + function_name="test_function", + ) + + with pytest.raises(ValueError, match="BaseToolAdapter requires a class-based tool"): + BaseToolAdapter(tool_info) + + def test_adapter_wrong_instance(self): + """Test BaseToolAdapter with wrong instance type.""" + tool_info = ToolInfo( + name="test_tool", + description="A test tool", + tool_type=ToolType.CLASS_BASED, + file_path="/test/path.py", + module_name="test_module", + class_name="TestTool", + tool_instance="not_a_tool", # Wrong type + ) + + with pytest.raises(ValueError, match="Tool instance must be a BaseTool instance"): + BaseToolAdapter(tool_info) + + @pytest.mark.asyncio + async def test_adapter_execute(self): + """Test executing a tool through BaseToolAdapter.""" + tool_instance = MockBaseTool() + tool_info = ToolInfo( + name="test_base_tool", + description="A test base tool", + tool_type=ToolType.CLASS_BASED, + file_path="/test/path.py", + module_name="test_module", + class_name="MockBaseTool", + tool_instance=tool_instance, + ) + + adapter = BaseToolAdapter(tool_info) + result = await adapter.execute({"message": "hello"}) + + assert result == {"processed": "Base tool processed: hello"} + + +class TestFunctionToolAdapter: + """Test FunctionToolAdapter functionality.""" + + def test_adapter_creation(self): + """Test creating a FunctionToolAdapter.""" + tool_info = ToolInfo( + name="test_function_tool", + description="A test function tool", + tool_type=ToolType.FUNCTION_BASED, + file_path="/test/path.py", + module_name="test_module", + function_name="test_function", + tool_instance=mock_function, + ) + + adapter = FunctionToolAdapter(tool_info) + assert adapter.name == "test_function_tool" + assert adapter.description == "A test function tool" + assert adapter.function == mock_function + + def test_adapter_wrong_type(self): + """Test FunctionToolAdapter with wrong tool type.""" + tool_info = ToolInfo( + name="test_tool", + description="A test tool", + tool_type=ToolType.CLASS_BASED, # Wrong type + file_path="/test/path.py", + module_name="test_module", + class_name="TestTool", + ) + + with pytest.raises(ValueError, match="FunctionToolAdapter requires a function-based tool"): + FunctionToolAdapter(tool_info) + + def test_adapter_not_callable(self): + """Test FunctionToolAdapter with non-callable instance.""" + tool_info = ToolInfo( + name="test_tool", + description="A test tool", + tool_type=ToolType.FUNCTION_BASED, + file_path="/test/path.py", + module_name="test_module", + function_name="test_function", + tool_instance="not_callable", # Not callable + ) + + with pytest.raises(ValueError, match="Tool instance must be callable"): + FunctionToolAdapter(tool_info) + + @pytest.mark.asyncio + async def test_adapter_execute_sync_function(self): + """Test executing a sync function through FunctionToolAdapter.""" + tool_info = ToolInfo( + name="test_function_tool", + description="A test function tool", + tool_type=ToolType.FUNCTION_BASED, + file_path="/test/path.py", + module_name="test_module", + function_name="test_function", + tool_instance=mock_function, + ) + + adapter = FunctionToolAdapter(tool_info) + result = await adapter.execute({"value": 5}) + + assert result == {"doubled": 10} + + @pytest.mark.asyncio + async def test_adapter_execute_async_function(self): + """Test executing an async function through FunctionToolAdapter.""" + tool_info = ToolInfo( + name="test_async_function_tool", + description="A test async function tool", + tool_type=ToolType.FUNCTION_BASED, + file_path="/test/path.py", + module_name="test_module", + function_name="test_async_function", + tool_instance=mock_async_function, + ) + + adapter = FunctionToolAdapter(tool_info) + result = await adapter.execute({"text": "hello"}) + + assert result == {"uppercased": "HELLO"} + + +class TestCreateAdapter: + """Test the create_adapter factory function.""" + + def test_create_base_tool_adapter(self): + """Test creating a BaseToolAdapter via factory.""" + tool_instance = MockBaseTool() + tool_info = ToolInfo( + name="test_base_tool", + description="A test base tool", + tool_type=ToolType.CLASS_BASED, + file_path="/test/path.py", + module_name="test_module", + class_name="MockBaseTool", + tool_instance=tool_instance, + ) + + adapter = create_adapter(tool_info) + assert isinstance(adapter, BaseToolAdapter) + assert adapter.name == "test_base_tool" + + def test_create_function_tool_adapter(self): + """Test creating a FunctionToolAdapter via factory.""" + tool_info = ToolInfo( + name="test_function_tool", + description="A test function tool", + tool_type=ToolType.FUNCTION_BASED, + file_path="/test/path.py", + module_name="test_module", + function_name="test_function", + tool_instance=mock_function, + ) + + adapter = create_adapter(tool_info) + assert isinstance(adapter, FunctionToolAdapter) + assert adapter.name == "test_function_tool" + + def test_create_adapter_unknown_type(self): + """Test create_adapter with unknown tool type.""" + # Use a mock ToolInfo that bypasses validation for testing + with pytest.raises(ValueError, match="Unknown tool type"): + create_adapter_with_invalid_type() + + def test_adapter_properties(self): + """Test adapter properties are correctly exposed.""" + tool_instance = MockBaseTool() + tool_info = ToolInfo( + name="test_base_tool", + description="A test base tool", + tool_type=ToolType.CLASS_BASED, + file_path="/test/path.py", + module_name="test_module", + class_name="MockBaseTool", + tool_instance=tool_instance, + parameters={"test": "param"}, + ) + + adapter = create_adapter(tool_info) + + assert adapter.name == "test_base_tool" + assert adapter.description == "A test base tool" + assert adapter.parameters == {"test": "param"} + + +def create_adapter_with_invalid_type(): + """Helper function to test invalid tool type.""" + # Create a mock tool info with invalid type by bypassing validation + from unittest.mock import Mock + + tool_info = Mock() + tool_info.tool_type = "unknown_type" + tool_info.name = "test_tool" + return create_adapter(tool_info) diff --git a/lib/serve/mcp-workbench/tests/test_core.py b/lib/serve/mcp-workbench/tests/test_core.py new file mode 100644 index 000000000..5cdf71841 --- /dev/null +++ b/lib/serve/mcp-workbench/tests/test_core.py @@ -0,0 +1,237 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for core components.""" + +from pathlib import Path + +import pytest +from mcpworkbench.core.annotations import get_tool_metadata, is_mcp_tool, mcp_tool +from mcpworkbench.core.base_tool import BaseTool, ToolInfo, ToolType +from mcpworkbench.core.tool_discovery import RescanResult, ToolDiscovery +from mcpworkbench.core.tool_registry import ToolRegistry + + +class TestBaseTool: + """Test BaseTool abstract class.""" + + def test_base_tool_instantiation(self): + """Test that BaseTool cannot be instantiated directly.""" + with pytest.raises(TypeError): + BaseTool("test", "test description") + + def test_concrete_tool_implementation(self): + """Test concrete implementation of BaseTool.""" + + class ConcreteTool(BaseTool): + def get_parameters(self): + return {} + + async def execute(self, **kwargs): + return {"result": "test"} + + tool = ConcreteTool("test_tool", "A test tool") + assert tool.name == "test_tool" + assert tool.description == "A test tool" + assert tool.get_parameters() == {} + + +class TestAnnotations: + """Test mcp_tool annotation functionality.""" + + def test_mcp_tool_decorator(self): + """Test the @mcp_tool decorator.""" + + @mcp_tool(name="test_func", description="A test function", parameters={"param1": "value1"}) + def test_function(): + return "test" + + assert is_mcp_tool(test_function) + metadata = get_tool_metadata(test_function) + assert metadata["name"] == "test_func" + assert metadata["description"] == "A test function" + assert metadata["parameters"] == {"param1": "value1"} + + def test_mcp_tool_without_parameters(self): + """Test @mcp_tool decorator without parameters.""" + + @mcp_tool(name="simple_func", description="Simple function") + def simple_function(): + return "simple" + + metadata = get_tool_metadata(simple_function) + assert metadata["parameters"] == {} + + def test_is_mcp_tool_false(self): + """Test is_mcp_tool returns False for regular functions.""" + + def regular_function(): + return "regular" + + assert not is_mcp_tool(regular_function) + + def test_get_tool_metadata_error(self): + """Test get_tool_metadata raises error for non-MCP tools.""" + + def regular_function(): + return "regular" + + with pytest.raises(ValueError, match="Function is not marked as an MCP tool"): + get_tool_metadata(regular_function) + + +class TestToolDiscovery: + """Test tool discovery functionality.""" + + def test_tool_discovery_init(self, temp_tools_dir: Path): + """Test ToolDiscovery initialization.""" + discovery = ToolDiscovery(str(temp_tools_dir)) + assert discovery.tools_directory == temp_tools_dir + + def test_tool_discovery_invalid_directory(self): + """Test ToolDiscovery with invalid directory.""" + with pytest.raises(ValueError, match="Tools directory does not exist"): + ToolDiscovery("/nonexistent/directory") + + def test_discover_tools(self, tool_discovery: ToolDiscovery): + """Test discovering tools from files.""" + tools = tool_discovery.discover_tools() + + # Should find both function and class-based tools + assert len(tools) == 3 # echo_test, add_test, greeting_test + + tool_names = [tool.name for tool in tools] + assert "echo_test" in tool_names + assert "add_test" in tool_names + assert "greeting_test" in tool_names + + # Check tool types + function_tools = [t for t in tools if t.tool_type == ToolType.FUNCTION_BASED] + class_tools = [t for t in tools if t.tool_type == ToolType.CLASS_BASED] + + assert len(function_tools) == 2 + assert len(class_tools) == 1 + + def test_rescan_tools(self, tool_discovery: ToolDiscovery): + """Test rescanning tools.""" + # Initial discovery + initial_tools = tool_discovery.discover_tools() + + # Rescan + rescan_result = tool_discovery.rescan_tools() + + assert isinstance(rescan_result, RescanResult) + assert rescan_result.total_tools == len(initial_tools) + + +class TestToolRegistry: + """Test tool registry functionality.""" + + def test_empty_registry(self, tool_registry: ToolRegistry): + """Test empty registry operations.""" + assert tool_registry.get_tool_count() == 0 + assert tool_registry.list_tools() == [] + assert tool_registry.list_tool_names() == [] + assert not tool_registry.has_tool("nonexistent") + assert tool_registry.get_tool("nonexistent") is None + + def test_register_single_tool(self, tool_registry: ToolRegistry): + """Test registering a single tool.""" + tool_info = ToolInfo( + name="test_tool", + description="A test tool", + tool_type=ToolType.FUNCTION_BASED, + file_path="/test/path.py", + module_name="test_module", + function_name="test_function", + ) + + tool_registry.register_tool(tool_info) + + assert tool_registry.get_tool_count() == 1 + assert tool_registry.has_tool("test_tool") + + retrieved_tool = tool_registry.get_tool("test_tool") + assert retrieved_tool is not None + assert retrieved_tool.name == "test_tool" + assert retrieved_tool.description == "A test tool" + + def test_register_multiple_tools(self, tool_registry: ToolRegistry, tool_discovery: ToolDiscovery): + """Test registering multiple tools.""" + tools = tool_discovery.discover_tools() + tool_registry.register_tools(tools) + + assert tool_registry.get_tool_count() == len(tools) + + tool_names = tool_registry.list_tool_names() + assert "echo_test" in tool_names + assert "add_test" in tool_names + assert "greeting_test" in tool_names + + def test_unregister_tool(self, tool_registry: ToolRegistry): + """Test unregistering a tool.""" + tool_info = ToolInfo( + name="test_tool", + description="A test tool", + tool_type=ToolType.FUNCTION_BASED, + file_path="/test/path.py", + module_name="test_module", + function_name="test_function", + ) + + tool_registry.register_tool(tool_info) + assert tool_registry.has_tool("test_tool") + + # Unregister existing tool + result = tool_registry.unregister_tool("test_tool") + assert result is True + assert not tool_registry.has_tool("test_tool") + + # Unregister non-existing tool + result = tool_registry.unregister_tool("nonexistent") + assert result is False + + def test_clear_registry(self, tool_registry: ToolRegistry, tool_discovery: ToolDiscovery): + """Test clearing the registry.""" + tools = tool_discovery.discover_tools() + tool_registry.register_tools(tools) + + assert tool_registry.get_tool_count() > 0 + + tool_registry.clear() + assert tool_registry.get_tool_count() == 0 + assert tool_registry.list_tools() == [] + + def test_update_registry(self, tool_registry: ToolRegistry, tool_discovery: ToolDiscovery): + """Test updating the registry with new tools.""" + # Add initial tool + initial_tool = ToolInfo( + name="initial_tool", + description="Initial tool", + tool_type=ToolType.FUNCTION_BASED, + file_path="/test/initial.py", + module_name="initial_module", + function_name="initial_function", + ) + tool_registry.register_tool(initial_tool) + assert tool_registry.get_tool_count() == 1 + + # Update with discovered tools + discovered_tools = tool_discovery.discover_tools() + tool_registry.update_registry(discovered_tools) + + # Should have only the discovered tools now + assert tool_registry.get_tool_count() == len(discovered_tools) + assert not tool_registry.has_tool("initial_tool") # Should be replaced + assert tool_registry.has_tool("echo_test") diff --git a/lib/serve/mcp-workbench/tests/test_integration.py b/lib/serve/mcp-workbench/tests/test_integration.py new file mode 100644 index 000000000..6d7d49bf8 --- /dev/null +++ b/lib/serve/mcp-workbench/tests/test_integration.py @@ -0,0 +1,53 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Integration tests for MCP Workbench server.""" + +import pytest + +FASTMCP_AVAILABLE = True + + +pytestmark = pytest.mark.skipif( + not FASTMCP_AVAILABLE, reason="FastMCP 2.0 not available - integration tests require FastMCP" +) + + +@pytest.mark.skipif(not FASTMCP_AVAILABLE, reason="FastMCP 2.0 required for integration tests") +def test_fastmcp_integration_placeholder(): + """Placeholder test for FastMCP 2.0 integration tests. + + Since we've migrated to pure FastMCP 2.0, these integration tests need to be + rewritten to test MCP protocol directly rather than REST API endpoints. + + TODO: Implement proper FastMCP 2.0 integration tests that: + 1. Start the FastMCP server + 2. Connect via MCP client + 3. Test tool discovery and execution via MCP protocol + 4. Test management tools (rescan_tools, exit_server) + """ + assert True # Placeholder - tests pass but indicate work needed + + +# Note: The previous REST API integration tests have been removed as they are no longer +# applicable to the pure FastMCP 2.0 architecture. New integration tests should: +# +# 1. Use an MCP client to connect to the server +# 2. Test tool discovery via MCP protocol +# 3. Test tool execution via MCP protocol +# 4. Test management tools as native MCP tools +# +# This requires either: +# - A proper MCP client implementation +# - Or mocking the MCP protocol for testing diff --git a/lib/serve/mcp-workbench/tests/test_manual.py b/lib/serve/mcp-workbench/tests/test_manual.py new file mode 100644 index 000000000..1575eef13 --- /dev/null +++ b/lib/serve/mcp-workbench/tests/test_manual.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python3 +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Test script for MCP Workbench. + +This script creates a temporary tools directory, populates it with example tools, +starts the MCP workbench server, and tests the API endpoints. +""" + +import subprocess +import sys +import tempfile +import time +from pathlib import Path + +import requests + + +def create_test_tools(tools_dir: Path): + """Create test tools in the given directory.""" + + # Create a simple function-based tool + function_tool = """ +from mcpworkbench.core.annotations import mcp_tool + +@mcp_tool( + name="echo", + description="Echo back the input text", + parameters={ + "type": "object", + "properties": { + "message": {"type": "string", "description": "Message to echo"} + }, + "required": ["message"] + } +) +def echo_message(message: str): + return {"echoed": message, "length": len(message)} + +@mcp_tool( + name="add_numbers", + description="Add two numbers together", + parameters={ + "type": "object", + "properties": { + "a": {"type": "number", "description": "First number"}, + "b": {"type": "number", "description": "Second number"} + }, + "required": ["a", "b"] + } +) +async def add_numbers(a: float, b: float): + return {"a": a, "b": b, "sum": a + b} +""" + + # Create a class-based tool + class_tool = """ +from mcpworkbench.core.base_tool import BaseTool + +class GreetingTool(BaseTool): + def __init__(self): + super().__init__( + name="greeting", + description="Generate personalized greetings" + ) + + def get_parameters(self): + return { + "type": "object", + "properties": { + "name": {"type": "string", "description": "Name to greet"}, + "style": { + "type": "string", + "enum": ["formal", "casual", "enthusiastic"], + "description": "Greeting style", + "default": "casual" + } + }, + "required": ["name"] + } + + async def execute(self, **kwargs): + name = kwargs["name"] + style = kwargs.get("style", "casual") + + if style == "formal": + greeting = f"Good day, {name}." + elif style == "enthusiastic": + greeting = f"Hey there, {name}! How exciting to meet you!" + else: # casual + greeting = f"Hi {name}!" + + return {"greeting": greeting, "name": name, "style": style} +""" + + # Write the tools to files + (tools_dir / "function_tools.py").write_text(function_tool) + (tools_dir / "class_tools.py").write_text(class_tool) + + print(f"Created test tools in {tools_dir}") + + +def check_api_endpoints(base_url: str): + """Test the API endpoints.""" + print(f"\nTesting API endpoints at {base_url}") + + try: + # Test list tools + print("1. Testing GET /mcp/tools") + response = requests.get(f"{base_url}/mcp/tools") + if response.status_code == 200: + tools = response.json() + print(f" ✓ Found {tools['count']} tools:") + for tool in tools["tools"]: + print(f" - {tool['name']}: {tool['description']}") + else: + print(f" ✗ Failed: {response.status_code} - {response.text}") + return False + + # Test get specific tool + print("\n2. Testing GET /mcp/tools/echo") + response = requests.get(f"{base_url}/mcp/tools/echo") + if response.status_code == 200: + tool = response.json() + print(f" ✓ Tool info: {tool['name']} - {tool['description']}") + else: + print(f" ✗ Failed: {response.status_code} - {response.text}") + + # Test call echo tool + print("\n3. Testing POST /mcp/tools/echo/call") + response = requests.post( + f"{base_url}/mcp/tools/echo/call", json={"arguments": {"message": "Hello, MCP Workbench!"}} + ) + if response.status_code == 200: + result = response.json() + print(f" ✓ Result: {result['result']}") + else: + print(f" ✗ Failed: {response.status_code} - {response.text}") + + # Test call add_numbers tool + print("\n4. Testing POST /mcp/tools/add_numbers/call") + response = requests.post(f"{base_url}/mcp/tools/add_numbers/call", json={"arguments": {"a": 15, "b": 27}}) + if response.status_code == 200: + result = response.json() + print(f" ✓ Result: {result['result']}") + else: + print(f" ✗ Failed: {response.status_code} - {response.text}") + + # Test call greeting tool + print("\n5. Testing POST /mcp/tools/greeting/call") + response = requests.post( + f"{base_url}/mcp/tools/greeting/call", json={"arguments": {"name": "Alice", "style": "enthusiastic"}} + ) + if response.status_code == 200: + result = response.json() + print(f" ✓ Result: {result['result']}") + else: + print(f" ✗ Failed: {response.status_code} - {response.text}") + + return True + + except requests.exceptions.ConnectionError: + print(f" ✗ Could not connect to server at {base_url}") + return False + except Exception as e: + print(f" ✗ Error: {e}") + return False + + +def main(): + """Main test function.""" + print("MCP Workbench Test Script") + print("=" * 40) + + # Create temporary directory for tools + with tempfile.TemporaryDirectory() as temp_dir: + tools_dir = Path(temp_dir) / "tools" + tools_dir.mkdir() + + # Create test tools + create_test_tools(tools_dir) + + # Start the server in the background + print("\nStarting MCP Workbench server...") + print(f"Tools directory: {tools_dir}") + + server_process = None + try: + # Start server + server_process = subprocess.Popen( + [ + sys.executable, + "-m", + "mcpworkbench.cli", + "--tools-dir", + str(tools_dir), + "--port", + "8001", + "--host", + "127.0.0.1", + "--rescan-route", + "/rescan", + "--verbose", + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + + # Wait for server to start + print("Waiting for server to start...") + time.sleep(3) + + # Check if server is running + if server_process.poll() is not None: + stdout, stderr = server_process.communicate() + print("Server failed to start:") + print(f"STDOUT: {stdout}") + print(f"STDERR: {stderr}") + return 1 + + # Test the API + success = check_api_endpoints("http://127.0.0.1:8001") + + if success: + print("\n" + "=" * 40) + print("✅ All tests passed!") + print("\nYou can now:") + print("1. Visit http://127.0.0.1:8001/mcp/tools to see available tools") + print("2. Use the rescan endpoint: curl -X POST http://127.0.0.1:8001/rescan") + print("3. Call tools via the API as demonstrated above") + print("\nPress Ctrl+C to stop the server...") + + # Keep server running for manual testing + try: + server_process.wait() + except KeyboardInterrupt: + print("\nShutting down server...") + + return 0 + else: + print("\n❌ Some tests failed") + return 1 + + except KeyboardInterrupt: + print("\nTest interrupted by user") + return 1 + except Exception as e: + print(f"\nError running test: {e}") + return 1 + finally: + if server_process and server_process.poll() is None: + print("Terminating server...") + server_process.terminate() + try: + server_process.wait(timeout=5) + except subprocess.TimeoutExpired: + server_process.kill() + + +if __name__ == "__main__": + exit(main()) diff --git a/lib/serve/mcpWorkbenchConstruct.ts b/lib/serve/mcpWorkbenchConstruct.ts new file mode 100644 index 000000000..5322e6247 --- /dev/null +++ b/lib/serve/mcpWorkbenchConstruct.ts @@ -0,0 +1,176 @@ +/** + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). +You may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { IAuthorizer, IRestApi, RestApi } from 'aws-cdk-lib/aws-apigateway'; +import { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; +import { Construct } from 'constructs'; +import { Vpc } from '../networking/vpc'; +import { BaseProps, Config } from '../schema'; +import * as s3 from 'aws-cdk-lib/aws-s3'; +import { RemovalPolicy, StackProps } from 'aws-cdk-lib'; +import { createCdkId } from '../core/utils'; +import * as ssm from 'aws-cdk-lib/aws-ssm'; +import { getDefaultRuntime, PythonLambdaFunction, registerAPIEndpoint } from '../api-base/utils'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import { LAMBDA_PATH } from '../util'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; + +export type McpWorkbenchConstructProps = { + authorizer: IAuthorizer; + restApiId: string; + rootResourceId: string; + securityGroups: ISecurityGroup[]; + vpc: Vpc; +} & BaseProps & StackProps; + +export default class McpWorkbenchConstruct extends Construct { + constructor (scope: Construct, id: string, props: McpWorkbenchConstructProps) { + super(scope, id); + + const { authorizer, config, restApiId, rootResourceId, securityGroups, vpc } = props; + + const workbenchBucket = this.createWorkbenchBucket(scope, config); + + // Get common layer based on arn from SSM due to issues with cross stack references + const commonLambdaLayer = lambda.LayerVersion.fromLayerVersionArn( + this, + 'mcp-common-lambda-layer', + ssm.StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/layerVersion/common`), + ); + + const fastapiLambdaLayer = lambda.LayerVersion.fromLayerVersionArn( + this, + 'mcp-fastapi-lambda-layer', + ssm.StringParameter.valueForStringParameter(this, `${config.deploymentPrefix}/layerVersion/fastapi`), + ); + + const restApi = RestApi.fromRestApiAttributes(this, 'RestApi', { + restApiId: restApiId, + rootResourceId: rootResourceId, + }); + + const lambdaLayers = [commonLambdaLayer, fastapiLambdaLayer]; + + this.createWorkbenchApi(restApi, rootResourceId, config, vpc, securityGroups, authorizer, workbenchBucket, lambdaLayers); + } + + private createWorkbenchApi (restApi: IRestApi, rootResourceId: string, config: Config, vpc: Vpc, securityGroups: ISecurityGroup[], authorizer: IAuthorizer, workbenchBucket: s3.Bucket, lambdaLayers: lambda.ILayerVersion[]) { + + const env = { + ADMIN_GROUP: config.authConfig?.adminGroup || '', + WORKBENCH_BUCKET: workbenchBucket.bucketName + }; + + // Create API Lambda functions + const apis: PythonLambdaFunction[] = [{ + name: 'list', + resource: 'mcp_workbench', + description: 'Lists available MCP Workbench tools', + method: 'GET', + environment: env, + path: 'mcp-workbench' + }, { + name: 'create', + resource: 'mcp_workbench', + description: 'Create MCP Workbench tools', + method: 'POST', + environment: env, + path: 'mcp-workbench' + }, { + name: 'read', + resource: 'mcp_workbench', + description: 'Get MCP Workbench tool', + method: 'GET', + environment: env, + path: 'mcp-workbench/{toolId}' + }, { + name: 'update', + resource: 'mcp_workbench', + description: 'Update MCP Workbench tool', + method: 'PUT', + environment: env, + path: 'mcp-workbench/{toolId}' + }, { + name: 'delete', + resource: 'mcp_workbench', + description: 'Delete MCP Workbench tool', + method: 'DELETE', + environment: env, + path: 'mcp-workbench/{toolId}' + }]; + + // Create IAM role for Lambda + const lambdaRole = new iam.Role(this, 'LambdaExecutionRole', { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + description: 'IAM role for Lambda function execution', + inlinePolicies: { + 'EC2NetworkInterfaces': new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + actions: ['ec2:CreateNetworkInterface', 'ec2:DescribeNetworkInterfaces', 'ec2:DeleteNetworkInterface'], + resources: ['*'], + }), + ], + }), + }, + }); + + // Attach AWSLambdaBasicExecutionRole policy to the role + lambdaRole.addManagedPolicy( + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole') + ); + + const lambdaPath = config.lambdaPath || LAMBDA_PATH; + apis.forEach((f) => { + const lambdaFunction = registerAPIEndpoint( + this, + restApi, + lambdaPath, + lambdaLayers, + f, + getDefaultRuntime(), + vpc, + securityGroups, + authorizer, + lambdaRole, + ); + if (f.method === 'POST' || f.method === 'PUT') { + workbenchBucket.grantWrite(lambdaFunction); + } else if (f.method === 'GET') { + workbenchBucket.grantRead(lambdaFunction); + } else if (f.method === 'DELETE') { + workbenchBucket.grantDelete(lambdaFunction); + } + }); + } + + private createWorkbenchBucket (scope: Construct, config: Config): s3.Bucket { + const bucketAccessLogsBucket = s3.Bucket.fromBucketArn(scope, 'BucketAccessLogsBucket', + ssm.StringParameter.valueForStringParameter(scope, `${config.deploymentPrefix}/bucket/bucket-access-logs`), + ); + + return new s3.Bucket(scope, createCdkId(['LISA', 'MCPWorkbench', config.deploymentName, config.deploymentStage]), { + bucketName: [config.deploymentName, config.deploymentStage, 'MCPWorkbench', config.accountNumber].join('-').toLowerCase(), + removalPolicy: config.removalPolicy, + autoDeleteObjects: config.removalPolicy === RemovalPolicy.DESTROY, + enforceSSL: true, + serverAccessLogsBucket: bucketAccessLogsBucket, + serverAccessLogsPrefix: 'logs/mcpworkbench-bucket/', + eventBridgeEnabled: true + }); + } +} diff --git a/lib/serve/serveApplicationConstruct.ts b/lib/serve/serveApplicationConstruct.ts index aa52264ad..a34c3edbf 100644 --- a/lib/serve/serveApplicationConstruct.ts +++ b/lib/serve/serveApplicationConstruct.ts @@ -39,6 +39,9 @@ import { LAMBDA_PATH, REST_API_PATH } from '../util'; import { AwsCustomResource, PhysicalResourceId } from 'aws-cdk-lib/custom-resources'; import { getDefaultRuntime } from '../api-base/utils'; import { ISecurityGroup, Port } from 'aws-cdk-lib/aws-ec2'; +import { ECSTasks } from '../api-base/ecsCluster'; +import { letIfDefined } from '../util/common-functions'; +import { EventBus } from 'aws-cdk-lib/aws-events'; export type LisaServeApplicationProps = { vpc: Vpc; @@ -90,6 +93,11 @@ export class LisaServeApplicationConstruct extends Construct { vpc: vpc, }); + // Create EventBus for management key rotation events + const managementEventBus = new EventBus(scope, createCdkId([scope.node.id, 'managementEventBus']), { + eventBusName: `${config.deploymentName}-lisa-management-events`, + }); + // Use a stable name for the management key secret const managementKeySecret = new Secret(scope, createCdkId([scope.node.id, 'managementKeySecret']), { secretName: `${config.deploymentName}-lisa-management-key`, // Use stable name based on deployment @@ -107,6 +115,9 @@ export class LisaServeApplicationConstruct extends Construct { handler: 'management_key.handler', code: Code.fromAsset(config.lambdaPath || LAMBDA_PATH), timeout: Duration.minutes(5), + environment: { + EVENT_BUS_NAME: managementEventBus.eventBusName, + }, role: new Role(scope, createCdkId([scope.node.id, 'managementKeyRotationRole']), { assumedBy: new ServicePrincipal('lambda.amazonaws.com'), managedPolicies: [ @@ -124,6 +135,13 @@ export class LisaServeApplicationConstruct extends Construct { 'secretsmanager:UpdateSecretVersionStage' ], resources: [managementKeySecret.secretArn] + }), + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'events:PutEvents' + ], + resources: [managementEventBus.eventBusArn] }) ] }) @@ -143,7 +161,9 @@ export class LisaServeApplicationConstruct extends Construct { parameterName: `${config.deploymentPrefix}/managementKeySecretName`, stringValue: managementKeySecret.secretName, }); - restApi.container.addEnvironment('MANAGEMENT_KEY_NAME', managementKeySecretNameStringParameter.stringValue); + restApi.containers.forEach((container) => { + container.addEnvironment('MANAGEMENT_KEY_NAME', managementKeySecretNameStringParameter.stringValue); + }); // LiteLLM requires a PostgreSQL database to support multiple-instance scaling with dynamic model management. const connectionParamName = 'LiteLLMDbConnectionInfo'; @@ -221,50 +241,58 @@ export class LisaServeApplicationConstruct extends Construct { ...(config.iamRdsAuth ? {} : { passwordSecretId: litellmDbPasswordSecret.secretName }) })); - - litellmDbConnectionInfoPs.grantRead(restApi.taskRole); + Object.values(restApi.taskRoles).forEach((taskRole) => { + litellmDbConnectionInfoPs.grantRead(taskRole); + }); // update the rdsConfig with the endpoint address config.restApiConfig.rdsConfig.dbHost = litellmDb.dbInstanceEndpointAddress; - if (config.iamRdsAuth) { - litellmDb.grantConnect(restApi.taskRole, restApi.taskRole.roleName); - - // Create the lambda for generating DB users for IAM auth - const createDbUserLambda = this.getIAMAuthLambda(scope, config, litellmDbPasswordSecret, restApi.taskRole.roleName, vpc, [litellmDbSg]); - - const customResourceRole = new Role(scope, 'LISAServeCustomResourceRole', { - assumedBy: new ServicePrincipal('lambda.amazonaws.com'), - }); - createDbUserLambda.grantInvoke(customResourceRole); - - // run updateInstanceKmsConditionsLambda every deploy - new AwsCustomResource(scope, 'LISAServeCreateDbUserCustomResource', { - onCreate: { - service: 'Lambda', - action: 'invoke', - physicalResourceId: PhysicalResourceId.of('LISAServeCreateDbUserCustomResource'), - parameters: { - FunctionName: createDbUserLambda.functionName, - Payload: '{}' + letIfDefined(restApi.taskRoles[ECSTasks.REST], (serveRole) => { + if (config.iamRdsAuth) { + litellmDb.grantConnect(serveRole, serveRole.roleName); + + // Create the lambda for generating DB users for IAM auth + const createDbUserLambda = this.getIAMAuthLambda(scope, config, litellmDbPasswordSecret, serveRole.roleName, vpc, [litellmDbSg]); + + const customResourceRole = new Role(scope, 'LISAServeCustomResourceRole', { + assumedBy: new ServicePrincipal('lambda.amazonaws.com'), + }); + createDbUserLambda.grantInvoke(customResourceRole); + + // run updateInstanceKmsConditionsLambda every deploy + new AwsCustomResource(scope, 'LISAServeCreateDbUserCustomResource', { + onCreate: { + service: 'Lambda', + action: 'invoke', + physicalResourceId: PhysicalResourceId.of('LISAServeCreateDbUserCustomResource'), + parameters: { + FunctionName: createDbUserLambda.functionName, + Payload: '{}' + }, }, - }, - role: customResourceRole - }); - } else { - litellmDb.grantConnect(restApi.taskRole); - litellmDbPasswordSecret.grantRead(restApi.taskRole); - } + role: customResourceRole + }); + } else { + litellmDb.grantConnect(serveRole); + litellmDbPasswordSecret.grantRead(serveRole); + } + }); - restApi.container.addEnvironment('LITELLM_DB_INFO_PS_NAME', litellmDbConnectionInfoPs.parameterName); + restApi.containers.forEach((container) => { + container.addEnvironment('LITELLM_DB_INFO_PS_NAME', litellmDbConnectionInfoPs.parameterName); + }); if (config.region.includes('iso')) { const ca_bundle = config.certificateAuthorityBundle ?? ''; - restApi.container.addEnvironment('SSL_CERT_DIR', '/etc/pki/tls/certs'); - restApi.container.addEnvironment('SSL_CERT_FILE', ca_bundle); - restApi.container.addEnvironment('REQUESTS_CA_BUNDLE', ca_bundle); - restApi.container.addEnvironment('CURL_CA_BUNDLE', ca_bundle); - restApi.container.addEnvironment('AWS_CA_BUNDLE', ca_bundle); + + restApi.containers.forEach((container) => { + container.addEnvironment('SSL_CERT_DIR', '/etc/pki/tls/certs'); + container.addEnvironment('SSL_CERT_FILE', ca_bundle); + container.addEnvironment('REQUESTS_CA_BUNDLE', ca_bundle); + container.addEnvironment('CURL_CA_BUNDLE', ca_bundle); + container.addEnvironment('AWS_CA_BUNDLE', ca_bundle); + }); } // Create Parameter Store entry with RestAPI URI @@ -281,9 +309,14 @@ export class LisaServeApplicationConstruct extends Construct { description: 'Serialized JSON of registered models data', }); - this.modelsPs.grantRead(restApi.taskRole); + letIfDefined(restApi.taskRoles[ECSTasks.REST], (serveRole) => { + this.modelsPs.grantRead(serveRole); + }); + // Add parameter as container environment variable for both RestAPI and RagAPI - restApi.container.addEnvironment('REGISTERED_MODELS_PS_NAME', this.modelsPs.parameterName); + restApi.containers.forEach((container) => { + container.addEnvironment('REGISTERED_MODELS_PS_NAME', this.modelsPs.parameterName); + }); restApi.node.addDependency(this.modelsPs); // Additional permissions for REST API Role @@ -311,7 +344,10 @@ export class LisaServeApplicationConstruct extends Construct { }), ] }); - restApi.taskRole.attachInlinePolicy(invocation_permissions); + letIfDefined(restApi.taskRoles[ECSTasks.REST], (serveRole) => { + this.modelsPs.grantRead(serveRole); + serveRole.attachInlinePolicy(invocation_permissions); + }); // Update this.restApi = restApi; diff --git a/lib/user-interface/react/package.json b/lib/user-interface/react/package.json index a3bea78f8..5963765db 100644 --- a/lib/user-interface/react/package.json +++ b/lib/user-interface/react/package.json @@ -1,7 +1,7 @@ { "name": "lisa-web", "private": true, - "version": "5.2.0", + "version": "5.3.0", "type": "module", "scripts": { "dev": "vite", @@ -13,10 +13,10 @@ "clean": "rm -rf ./dist ./node_modules" }, "dependencies": { - "@cloudscape-design/chat-components": "^1.0.22", - "@cloudscape-design/collection-hooks": "^1.0.59", - "@cloudscape-design/component-toolkit": "^1.0.0-beta.65", - "@cloudscape-design/components": "^3.0.883", + "@cloudscape-design/chat-components": "^1.0.61", + "@cloudscape-design/collection-hooks": "^1.0.74", + "@cloudscape-design/component-toolkit": "^1.0.0-beta.114", + "@cloudscape-design/components": "^3.0.1071", "@cloudscape-design/global-styles": "^1.0.35", "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-regular-svg-icons": "^6.7.2", @@ -27,6 +27,7 @@ "@microsoft/fetch-event-source": "^2.0.1", "@reduxjs/toolkit": "^1.9.7", "@swc/core": "^1.11.8", + "ace-builds": "^1.43.2", "axios": "^1.8.2", "git-repo-info": "^2.1.1", "jszip": "^3.10.1", @@ -35,6 +36,7 @@ "luxon": "^3.5.0", "mermaid": "^11.10.1", "react": "^18.3.1", + "react-ace": "^14.0.1", "react-dom": "^18.3.1", "react-json-view-lite": "^0.9.8", "react-markdown": "^9.0.3", @@ -55,6 +57,7 @@ "vitepress": "^1.6.4" }, "devDependencies": { + "@types/ace": "^0.0.52", "@types/markdown-it": "^14.1.2", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", diff --git a/lib/user-interface/react/src/App.tsx b/lib/user-interface/react/src/App.tsx index 116a71a27..724880b5c 100644 --- a/lib/user-interface/react/src/App.tsx +++ b/lib/user-interface/react/src/App.tsx @@ -42,6 +42,7 @@ import PromptTemplatesLibrary from './pages/PromptTemplatesLibrary'; import { ConfigurationContext } from './shared/configuration.provider'; import McpServers from '@/pages/Mcp'; import ModelComparisonPage from './pages/ModelComparison'; +import McpWorkbench from './pages/McpWorkbench'; export type RouteProps = { @@ -199,6 +200,14 @@ function App () { } />} + {config?.configuration?.enabledComponents?.showMcpWorkbench && + + } + /> + } {config?.configuration?.enabledComponents?.enableModelComparisonUtility && (undefined); const [modelFilterValue, setModelFilterValue] = useState(''); const [hasUserInteractedWithModel, setHasUserInteractedWithModel] = useState(false); + const [mermaidRenderComplete, setMermaidRenderComplete] = useState(0); + + // Callback to handle Mermaid diagram rendering completion + const handleMermaidRenderComplete = useCallback(() => { + setMermaidRenderComplete((prev) => prev + 1); + }, []); // Ref to track if we're processing tool calls to prevent infinite loops const isProcessingToolCalls = useRef(false); @@ -449,7 +455,7 @@ export default function Chat ({ sessionId }) { if (bottomRef.current) { bottomRef.current.scrollIntoView({ behavior: 'smooth' }); } - }, [session.history.length, isStreaming, isRunning, generateResponse]); + }, [isStreaming, session, mermaidRenderComplete]); // Reset tool call counter when session changes useEffect(() => { @@ -685,6 +691,7 @@ export default function Chat ({ sessionId }) { handleSendGenerateRequest={handleSendGenerateRequest} chatConfiguration={chatConfiguration} setUserPrompt={setUserPrompt} + onMermaidRenderComplete={handleMermaidRenderComplete} /> ))} {(isRunning || callingToolName) && !isStreaming && } {session.history.length === 0 && sessionId === undefined && ( void; }; -const MermaidDiagram: React.FC = React.memo(({ chart, id, isStreaming }) => { +const MermaidDiagram: React.FC = React.memo(({ chart, id, isStreaming, onRenderComplete }) => { const containerRef = useRef(null); const [error, setError] = useState(''); const [svg, setSvg] = useState(''); @@ -40,20 +43,7 @@ const MermaidDiagram: React.FC = React.memo(({ chart, id, i startOnLoad: false, theme: 'dark', securityLevel: 'loose', - fontFamily: 'Arial, sans-serif', suppressErrorRendering: true, - fontSize: 14, - flowchart: { - useMaxWidth: true, - htmlLabels: true, - }, - sequence: { - useMaxWidth: true, - wrap: true, - }, - gantt: { - useMaxWidth: true, - }, }); mermaidInitialized.current = true; } @@ -93,11 +83,15 @@ const MermaidDiagram: React.FC = React.memo(({ chart, id, i setError(`Failed to render diagram: ${err instanceof Error ? err.message : 'Unknown error'}`); } finally { setIsLoading(false); + // Call the callback when rendering is complete (success or error) + if (onRenderComplete) { + onRenderComplete(); + } } }; renderDiagram(); - }, [chart, id, svg, isStreaming]); + }, [chart, id, svg, isStreaming, onRenderComplete]); const copyToClipboard = useCallback(async (content: string) => { try { @@ -129,8 +123,8 @@ const MermaidDiagram: React.FC = React.memo(({ chart, id, i border: '1px solid #d13212', borderRadius: '4px', color: '#ff6b6b', - fontFamily: 'monospace', - fontSize: '12px' + fontSize: '12px', + textWrap: 'wrap' }}> Mermaid Error: {error}
@@ -218,7 +212,9 @@ const MermaidDiagram: React.FC = React.memo(({ chart, id, i borderRadius: '4px', overflow: 'auto' }} - dangerouslySetInnerHTML={{ __html: svg }} + dangerouslySetInnerHTML={{ + __html: DOMPurify.sanitize(svg, MERMAID_SANITIZATION_CONFIG) + }} /> ); diff --git a/lib/user-interface/react/src/components/chatbot/components/Message.tsx b/lib/user-interface/react/src/components/chatbot/components/Message.tsx index f47cb0761..52b6df925 100644 --- a/lib/user-interface/react/src/components/chatbot/components/Message.tsx +++ b/lib/user-interface/react/src/components/chatbot/components/Message.tsx @@ -53,9 +53,10 @@ type MessageProps = { setUserPrompt: (state: string) => void; chatConfiguration: IChatConfiguration; showUsage?: boolean; + onMermaidRenderComplete?: () => void; }; -export default function Message ({ message, isRunning, showMetadata, isStreaming, markdownDisplay, setUserPrompt, setChatConfiguration, handleSendGenerateRequest, chatConfiguration, callingToolName, showUsage = false }: MessageProps) { +export default function Message ({ message, isRunning, showMetadata, isStreaming, markdownDisplay, setUserPrompt, setChatConfiguration, handleSendGenerateRequest, chatConfiguration, callingToolName, showUsage = false, onMermaidRenderComplete }: MessageProps) { const currentUser = useAppSelector(selectCurrentUsername); const ragCitations = !isStreaming && message?.metadata?.ragDocuments ? message?.metadata.ragDocuments : undefined; const [resend, setResend] = useState(false); @@ -129,7 +130,7 @@ export default function Message ({ message, isRunning, showMetadata, isStreaming position: 'absolute', top: '5px', right: '5px', - zIndex: 10 + zIndex: 10, }} > @@ -198,7 +200,7 @@ export default function Message ({ message, isRunning, showMetadata, isStreaming } return match ? ( match[1] === 'mermaid' ? ( - + ) : ( ); }, - }), [isStreaming]); // Include isStreaming so the component can access it + }), [isStreaming, onMermaidRenderComplete]); // Include isStreaming and onMermaidRenderComplete so the component can access them const renderContent = (messageType: string, content: MessageContent, metadata?: LisaChatMessageMetadata) => { if (Array.isArray(content)) { diff --git a/lib/user-interface/react/src/components/chatbot/config/mermaidSanitizationConfig.ts b/lib/user-interface/react/src/components/chatbot/config/mermaidSanitizationConfig.ts new file mode 100644 index 000000000..4e2b63a09 --- /dev/null +++ b/lib/user-interface/react/src/components/chatbot/config/mermaidSanitizationConfig.ts @@ -0,0 +1,31 @@ +/** + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +export const MERMAID_SANITIZATION_CONFIG = { + // foreignObject Tag is necessary for text in diagrams (flow, class, etc) + ADD_TAGS: ['foreignObject', 'p', 'br'], + HTML_INTEGRATION_POINTS: {'foreignobject': true}, + // Various attributes that are necessary for diagram formatting + ADD_ATTR: [ + 'text-anchor', 'dominant-baseline', 'font-family', 'font-size', 'font-weight', 'font-style', + 'x', 'y', 'dx', 'dy', 'rx', 'ry', 'rotate', 'textLength', 'lengthAdjust', + 'startOffset', 'method', 'spacing', 'alignment-baseline', 'baseline-shift', + 'letter-spacing', 'word-spacing', 'text-decoration', 'text-rendering', + 'unicode-bidi', 'direction', 'writing-mode', 'glyph-orientation-vertical', + 'glyph-orientation-horizontal', 'kerning', 'fill', 'stroke', 'stroke-width', + 'opacity', 'fill-opacity', 'stroke-opacity', 'transform', 'style', 'width', 'height', + ] +}; diff --git a/lib/user-interface/react/src/components/configuration/ActivatedUserComponents.tsx b/lib/user-interface/react/src/components/configuration/ActivatedUserComponents.tsx index 4c4ee7abb..0f5127b34 100644 --- a/lib/user-interface/react/src/components/configuration/ActivatedUserComponents.tsx +++ b/lib/user-interface/react/src/components/configuration/ActivatedUserComponents.tsx @@ -15,7 +15,7 @@ */ import { Box, Container, Grid, Header, SpaceBetween, Toggle } from '@cloudscape-design/components'; -import React from 'react'; +import React, { useCallback } from 'react'; import { SetFieldsFunction } from '../../shared/validation'; const ragOptions = { @@ -32,7 +32,6 @@ const libraryOptions = { const inContextOptions = { uploadContextDocs: 'Allow document upload to context', documentSummarization: 'Allow Document Summarization', - mcpConnections: 'Allow MCP Server Connections', }; const advancedOptions = { @@ -44,6 +43,36 @@ const advancedOptions = { enableModelComparisonUtility: 'Enable Model Comparison Utility' }; +const mcpOptions = { + mcpConnections: 'Allow MCP Server Connections', + showMcpWorkbench: 'Show MCP Workbench' +}; + +const optionGroups = { + mcpOptions, + inContextOptions, + ragOptions, + libraryOptions, + advancedOptions, +} as const; + +type AllOptionKeys>> = { + [K in keyof G]: keyof G[K]; +}[keyof G]; + +type DependencyMap>> = { + [Opt in AllOptionKeys]?: { + prerequisites?: ReadonlyArray>; + dependents?: ReadonlyArray>; + }; +}; + + +const dependencies: DependencyMap = { + showMcpWorkbench: {prerequisites: ['mcpConnections'] }, + mcpConnections: {dependents: ['showMcpWorkbench']} +}; + const configurableOperations = [{ header: 'RAG Components', items: ragOptions @@ -59,6 +88,9 @@ const configurableOperations = [{ { header: 'Advanced Components', items: advancedOptions +}, { + header: 'MCP Components', + items: mcpOptions }]; export type ActivatedComponentConfigurationProps = { @@ -67,6 +99,46 @@ export type ActivatedComponentConfigurationProps = { }; export function ActivatedUserComponents (props: ActivatedComponentConfigurationProps) { + const { setFields } = props; + // Helper function to check if an option should be disabled based on prerequisites + const isOptionDisabled = useCallback((optionKey: string): boolean => { + const dependency = dependencies[optionKey]; + return Boolean(dependency?.prerequisites?.some((prereq) => !props.enabledComponents[prereq])); + }, [props.enabledComponents]); + + // Helper function to recursively collect all dependents + const getAllDependents = useCallback((optionKey: string, visited = new Set()): string[] => { + if (visited.has(optionKey)) return []; + visited.add(optionKey); + + const dependency = dependencies[optionKey]; + if (!dependency?.dependents) return []; + + const allDependents: string[] = []; + for (const dependent of dependency.dependents) { + allDependents.push(dependent); + allDependents.push(...getAllDependents(dependent, visited)); + } + + return allDependents; + }, []); + + // Handle toggle changes with dependency management + const handleToggleChange = useCallback((item: string, checked: boolean) => { + const updatedFields: Record = {}; + updatedFields[`enabledComponents.${item}`] = checked; + + // If turning off, also turn off all dependents recursively + if (!checked) { + const allDependents = getAllDependents(item); + for (const dependent of allDependents) { + updatedFields[`enabledComponents.${dependent}`] = false; + } + } + + setFields(updatedFields); + }, [setFields, getAllDependents]); + return ( ({ colspan: 4 }))}> {configurableOperations.map((operation) => - +
{operation.header}
{Object.keys(operation.items).map((item) => { + const isDisabled = isOptionDisabled(item); + const isChecked = props.enabledComponents[item] || false; + return ( { - const updatedField = {}; - updatedField[`enabledComponents.${item}`] = detail.checked; - props.setFields(updatedField); + handleToggleChange(item, detail.checked); }} - checked={props.enabledComponents[item] || false} + checked={isChecked} + disabled={isDisabled} data-cy={`Toggle-${item}`} > {operation.items[item]} diff --git a/lib/user-interface/react/src/components/configuration/ConfigurationComponent.tsx b/lib/user-interface/react/src/components/configuration/ConfigurationComponent.tsx index 2ba7c3b9e..7e5e90bab 100644 --- a/lib/user-interface/react/src/components/configuration/ConfigurationComponent.tsx +++ b/lib/user-interface/react/src/components/configuration/ConfigurationComponent.tsx @@ -29,6 +29,7 @@ import { getJsonDifference } from '../../shared/util/validationUtils'; import { setConfirmationModal } from '../../shared/reducers/modal.reducer'; import { useNotificationService } from '../../shared/util/hooks'; import RepositoryTable from './RepositoryTable'; +import { mcpServerApi } from '@/shared/reducers/mcp-server.reducer'; export type ConfigState = { validateAll: boolean; @@ -115,6 +116,10 @@ export function ConfigurationComponent (): ReactElement { useEffect(() => { if (!isUpdating && isUpdateSuccess) { notificationService.generateNotification('Successfully updated configuration', 'success'); + + // invalidate the mcp servers on update in case they've changed + dispatch(mcpServerApi.util.invalidateTags(['mcpServers'])); + resetUpdate(); } else if (!isUpdating && isUpdateError) { notificationService.generateNotification(`Error updating config: ${updateError.data?.message ?? updateError.data}`, 'error'); diff --git a/lib/user-interface/react/src/components/mcp-workbench/McpWorkbenchManagementComponent.tsx b/lib/user-interface/react/src/components/mcp-workbench/McpWorkbenchManagementComponent.tsx new file mode 100644 index 000000000..220d2ef01 --- /dev/null +++ b/lib/user-interface/react/src/components/mcp-workbench/McpWorkbenchManagementComponent.tsx @@ -0,0 +1,413 @@ +/** + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { Button, CodeEditor, Container, Grid, SpaceBetween, List, Header, Box, Input, FormField, TextFilter, Pagination } from '@cloudscape-design/components'; +import 'react'; +import 'ace-builds'; +import ace from 'ace-builds'; +import 'ace-builds/src-noconflict/mode-python'; +import 'ace-builds/src-noconflict/theme-tomorrow'; +import 'ace-builds/src-noconflict/ext-language_tools'; +import { ReactElement, useEffect, useState } from 'react'; +import { useAppDispatch } from '@/config/store'; +import { useNotificationService } from '@/shared/util/hooks'; +import * as z from 'zod'; +import { setConfirmationModal } from '@/shared/reducers/modal.reducer'; +import { + useListMcpToolsQuery, + useGetMcpToolQuery, + useCreateMcpToolMutation, + useUpdateMcpToolMutation, + useDeleteMcpToolMutation, + mcpToolsApi +} from '@/shared/reducers/mcp-tools.reducer'; +import { IMcpTool } from '@/shared/model/mcp-tools.model'; +import { setBreadcrumbs } from '@/shared/reducers/breadcrumbs.reducer'; +import { useValidationReducer } from '@/shared/validation'; + +const DEFAULT_CONTENT = 'from mcpworkbench.core.annotations import mcp_tool\nfrom mcpworkbench.core.base_tool import BaseTool\nfrom typing import Annotated\n\n\n# =============================================================================\n# METHOD 1: FUNCTION-BASED APPROACH WITH @mcp_tool DECORATOR\n# =============================================================================\n# This is a simpler approach for straightforward tools that don\'t need\n# complex initialization or state management.\n\n@mcp_tool(\n name="simple_calculator",\n description="A simple calculator using the decorator approach"\n)\nasync def simple_calculator(\n operator: Annotated[str, "The arithmetic operation: add, subtract, multiply, or divide"],\n left_operand: Annotated[float, "The first number in the operation"],\n right_operand: Annotated[float, "The second number in the operation"]\n) -> dict:\n \'\'\'\n Perform basic arithmetic operations using the decorator approach.\n \n The @mcp_tool decorator automatically:\n 1. Registers the function as an MCP tool\n 2. Extracts parameter information from type annotations\n 3. Uses the Annotated descriptions for parameter documentation\n 4. Handles the MCP protocol communication\n \n This approach is ideal for:\n - Simple, stateless operations\n - Quick prototyping\n - Tools that don\'t need complex initialization\n \'\'\'\n \n if operator == "add":\n result = left_operand + right_operand\n elif operator == "subtract":\n result = left_operand - right_operand\n elif operator == "multiply":\n result = left_operand * right_operand\n elif operator == "divide":\n if right_operand == 0:\n raise ValueError("Cannot divide by zero")\n result = left_operand / right_operand\n else:\n raise ValueError(f"Unknown operator: {operator}")\n \n return {\n "operator": operator,\n "left_operand": left_operand,\n "right_operand": right_operand,\n "result": result\n }\n\n\n# =============================================================================\n# METHOD 2: CLASS-BASED APPROACH\n# =============================================================================\n# This is the more structured approach, ideal for complex tools that need\n# initialization, state management, or multiple related operations.\n\nclass CalculatorTool(BaseTool):\n """\n A simple calculator tool that performs basic arithmetic operations.\n \n This class demonstrates the class-based approach to creating MCP tools:\n 1. Inherit from BaseTool\n 2. Initialize with name and description in __init__\n 3. Implement execute() method that returns the callable function\n 4. Define the actual tool function with proper type annotations\n """\n \n def __init__(self):\n """\n Initialize the tool with metadata.\n \n The BaseTool constructor requires:\n - name: A unique identifier for the tool\n - description: A clear description of what the tool does\n """\n super().__init__(\n name="calculator",\n description="Performs basic arithmetic operations (add, subtract, multiply, divide)"\n )\n\n async def execute(self):\n """\n Return the callable function that implements the tool\'s functionality.\n \n This method is called by the MCP framework to get the actual function\n that will be executed when the tool is invoked.\n """\n return self.calculate\n \n async def calculate(\n self,\n operator: Annotated[str, "add, subtract, multiply, or divide"],\n left_operand: Annotated[float, "The first number"],\n right_operand: Annotated[float, "The second number"]\n ):\n """\n Execute the calculator operation.\n \n Parameter Type Annotations with Context:\n =======================================\n Notice the use of Annotated[type, "description"] for each parameter.\n This is OPTIONAL but highly recommended because it provides:\n \n 1. Type information for the MCP framework\n 2. Human-readable descriptions that help AI models understand\n what each parameter is for\n 3. Better error messages and validation\n \n The Annotated type comes from typing module and follows this pattern:\n Annotated[actual_type, "description_string"]\n \n Examples:\n - Annotated[str, "The operation to perform"]\n - Annotated[int, "A positive integer between 1 and 100"]\n - Annotated[list[str], "A list of file paths to process"]\n """ \n if operator == "add":\n result = left_operand + right_operand\n elif operator == "subtract":\n result = left_operand - right_operand\n elif operator == "multiply":\n result = left_operand * right_operand\n elif operator == "divide":\n if right_operand == 0:\n raise ValueError("Cannot divide by zero")\n result = left_operand / right_operand\n else:\n raise ValueError(f"Unknown operator: {operator}")\n \n return {\n "operator": operator,\n "left_operand": left_operand,\n "right_operand": right_operand,\n "result": result\n }'; + +export function McpWorkbenchManagementComponent (): ReactElement { + const dispatch = useAppDispatch(); + const notificationService = useNotificationService(dispatch); + + // API hooks + const { data: tools = [], isFetching: isLoadingTools, refetch } = useListMcpToolsQuery(); + const [selectedToolId, setSelectedToolId] = useState(null); + const { data: selectedToolData, isFetching: isLoadingTool, } = useGetMcpToolQuery(selectedToolId!, { + refetchOnMountOrArgChange: true, + refetchOnFocus: true + }); + + const [createToolMutation, { isLoading: isCreating }] = useCreateMcpToolMutation(); + const [updateToolMutation, { isLoading: isUpdating }] = useUpdateMcpToolMutation(); + const [deleteToolMutation] = useDeleteMcpToolMutation(); + + const [isDirty, setIsDirty] = useState(false); + + const schema = z.object({ + id: z.string().regex(/^[a-z0-9_.]+?(\.py)?$/).trim().min(3, 'String cannot be empty.'), + contents: z.string().trim().min(1, 'String cannot be empty.'), + }); + + const { errors, touchFields, setFields, isValid, state } = useValidationReducer(schema, { + form: { id: `my_new_tool_${Date.now()}`, contents: DEFAULT_CONTENT} as IMcpTool, + formSubmitting: false, + touched: {}, + validateAll: false + }); + + // Filtering and pagination state + const [filterText, setFilterText] = useState(''); + const [currentPageIndex, setCurrentPageIndex] = useState(1); + const pageSize = 5; + + // Filter and paginate tools + const filteredTools = tools.filter((tool) => + tool.id.toLowerCase().includes(filterText.toLowerCase()) || + tool.contents?.toLowerCase().includes(filterText.toLowerCase()) + ); + + const totalPages = Math.ceil(filteredTools.length / pageSize); + const paginatedTools = filteredTools.slice( + (currentPageIndex - 1) * pageSize, + currentPageIndex * pageSize + ); + + // remove top breadcrumbs + dispatch(setBreadcrumbs([])); + + // Reset pagination when filter changes + useEffect(() => { + setCurrentPageIndex(1); + }, [filterText]); + + // Update editor content when a tool is selected + useEffect(() => { + if (selectedToolId !== null && selectedToolData && isDirty === false) { + setFields({ + id: selectedToolData.id, + contents: selectedToolData.contents, + size: selectedToolData.size, + updated_at : selectedToolData.updated_at + }); + setIsDirty(true); + } + }, [selectedToolId, selectedToolData, setFields, isDirty]); + + // Handle tool selection + const handleToolSelect = (tool: IMcpTool) => { + if (isDirty) { + dispatch( + setConfirmationModal({ + action: 'Switch Tool?', + resourceName: 'Unsaved change', + onConfirm: () => { + setSelectedToolId(tool.id); + setIsDirty(false); + }, + description: 'You have unsaved changes. Switching tools will lose these changes.' + }) + ); + } else { + setSelectedToolId(tool.id); + } + }; + + // Handle editor content change + const handleEditorChange = (value: string) => { + setFields({ + contents: value + }); + setIsDirty(true); + touchFields(['contents']); + }; + + // Handle creating new tool + const handleCreateNew = () => { + const newTool = { + id: ['my_new_tool', Date.now()].join('-'), + contents: defaultContent, + }; + + if (isDirty && selectedToolId) { + dispatch( + setConfirmationModal({ + action: 'Create New Tool', + resourceName: '', + onConfirm: () => { + setSelectedToolId(null); + setFields(newTool); + setIsDirty(false); + }, + description: 'You have unsaved changes. Creating a new tool will lose these changes.' + }) + ); + } else { + setSelectedToolId(null); + setFields(newTool); + setIsDirty(true); + } + }; + + // Handle create tool + const handleCreateTool = async () => { + try { + await createToolMutation({ + id: state.form.id, + contents: state.form.contents + }).unwrap(); + + notificationService.generateNotification(`Successfully created tool: ${state.form.id}`, 'success'); + setIsDirty(false); + dispatch(mcpToolsApi.util.invalidateTags(['mcpTools'])); + refetch(); + } catch (error: any) { + const errorMessage = error?.data?.message || error?.message || 'Unknown error occurred'; + notificationService.generateNotification(`Error creating tool: ${errorMessage}`, 'error'); + } + }; + + // Handle update tool + const handleUpdateTool = async () => { + if (!selectedToolId || !selectedToolData) return; + + try { + await updateToolMutation({ + toolId: selectedToolId, + tool: { contents: state.form.contents } + }).unwrap(); + + notificationService.generateNotification(`Successfully updated tool: ${selectedToolId}`, 'success'); + setIsDirty(false); + dispatch(mcpToolsApi.util.invalidateTags(['mcpTools'])); + } catch (error: any) { + const errorMessage = error?.data?.message || error?.message || 'Unknown error occurred'; + notificationService.generateNotification(`Error updating tool: ${errorMessage}`, 'error'); + } + }; + + // Handle delete tool + const handleDeleteTool = (tool: IMcpTool) => { + dispatch( + setConfirmationModal({ + action: 'Delete', + resourceName: 'Tool', + onConfirm: async () => { + try { + await deleteToolMutation(tool.id).unwrap(); + notificationService.generateNotification(`Successfully deleted tool: ${tool.id}`, 'success'); + + // Reset selection if the deleted tool was selected + if (selectedToolId === tool.id) { + setSelectedToolId(null); + setFields({ + id: '', + contents: '', + size: undefined, + updated_at : undefined + }); + setIsDirty(false); + } + + refetch(); + } catch (error: any) { + const errorMessage = error?.data?.message || error?.message || 'Unknown error occurred'; + notificationService.generateNotification(`Error deleting tool: ${errorMessage}`, 'error'); + } + }, + description: `This will permanently delete the tool: ${tool.id}` + }) + ); + }; + + return ( + + MCP Workbench Editor + + }> + + +
+ {/* */} + + + } + > + Tool Files ({tools.length}) +
+ + {tools.length > 0 && ( + setFilterText(detail.filteringText)} + /> + )} + + {isLoadingTools ? ( + + + Loading tools... + + + ) : tools.length === 0 ? ( + + + No tools +

Create your first tool to get started.

+
+
+ ) : filteredTools.length === 0 ? ( + + + No tools match your search +

Try adjusting your search criteria.

+
+
+ ) : ( + <> + ({ + id: item.id, + content: ( + + + {item.updated_at && ( +
+ Updated: {new Date(item.updated_at).toLocaleString()} +
+ )} + {item.size && ( +
+ Size: {item.size} bytes +
+ )} +
+ ), + actions: ( + + ) : ( + + )} +
+
+
+
+ ); +} + +export default McpWorkbenchManagementComponent; diff --git a/lib/user-interface/react/src/components/mcp/McpServerDetails.tsx b/lib/user-interface/react/src/components/mcp/McpServerDetails.tsx index 9bfc2cd40..cec09cfe8 100644 --- a/lib/user-interface/react/src/components/mcp/McpServerDetails.tsx +++ b/lib/user-interface/react/src/components/mcp/McpServerDetails.tsx @@ -123,7 +123,7 @@ export function McpServerDetails () { } if (isUninitialized && mcpServerId) { - getMcpServerQuery(mcpServerId); + getMcpServerQuery({mcpServerId}); } const { diff --git a/lib/user-interface/react/src/components/mcp/McpServerForm.tsx b/lib/user-interface/react/src/components/mcp/McpServerForm.tsx index 947859c04..6a269635f 100644 --- a/lib/user-interface/react/src/components/mcp/McpServerForm.tsx +++ b/lib/user-interface/react/src/components/mcp/McpServerForm.tsx @@ -26,12 +26,14 @@ import { Toggle, StatusIndicator, Box, + TokenGroup, } from '@cloudscape-design/components'; +import { KeyCode } from '@cloudscape-design/component-toolkit/internal'; import 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import { scrollToInvalid, useValidationReducer } from '../../shared/validation'; +import { issuesToErrors, scrollToInvalid, useValidationReducer } from '../../shared/validation'; import { z } from 'zod'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { setBreadcrumbs } from '../../shared/reducers/breadcrumbs.reducer'; import { useAppDispatch, useAppSelector } from '../../config/store'; import { useNotificationService } from '../../shared/util/hooks'; @@ -39,6 +41,7 @@ import { ModifyMethod } from '../../shared/validation/modify-method'; import { selectCurrentUserIsAdmin, selectCurrentUsername } from '../../shared/reducers/user.reducer'; import { DefaultMcpServer, + mcpServerApi, McpServerStatus, NewMcpServer, useCreateMcpServerMutation, @@ -91,11 +94,24 @@ export function McpServerForm (props: McpServerFormProps) { validateAll: false }); + // handle separate token text validation + const [tokenText, setTokenText] = useState(''); + const tokenTextSchema = z.object({'groups': z.string().trim().max(0, {message: 'You must press return to add a group.'})}); + const tokenTextResult = tokenTextSchema.safeParse({'groups': tokenText}); + const tokenTextErrors = tokenTextResult.success ? undefined : issuesToErrors(tokenTextResult?.error?.issues || [], state.touched); + + // memoize conversion to tokens + const tokens = useMemo(() => { + return state.form.groups + .filter((group) => group !== 'lisa:public') + .map((group) => ({label: group.replace(/^\w+:/, '')})); + }, [state.form.groups]); + const canEdit = mcpServerId ? (isUserAdmin || data?.isOwner) : true; const disabled = isFetching || isCreating || isUpdating; if (isEdit && isUninitialized && mcpServerId) { - getMcpServerQuery(mcpServerId).then((response) => { + getMcpServerQuery({mcpServerId, showPlaceholder: true}).then((response) => { if (response.isSuccess) { setFields({ ...response.data, customHeaders: response.data.customHeaders ? Object.entries(response.data.customHeaders).map(([key, value]) => ({ key, value })) : [], @@ -179,8 +195,9 @@ export function McpServerForm (props: McpServerFormProps) { const data = isCreatingSuccess ? createData : updateData; notificationService.generateNotification(`Successfully ${verb} MCP Connection: ${data.name}`, 'success'); navigate(`/mcp-connections/${data.id}`); + dispatch(mcpServerApi.util.invalidateTags(['mcpServers'])); } - }, [isCreatingSuccess, isUpdatingSuccess, notificationService, createData, updateData, navigate]); + }, [isCreatingSuccess, isUpdatingSuccess, notificationService, createData, updateData, navigate, dispatch]); // create failure notification useEffect(() => { @@ -267,23 +284,55 @@ export function McpServerForm (props: McpServerFormProps) { )} - {isUserAdmin && - - { - setSharePublic(detail.checked); - setFields({owner: detail.checked ? 'lisa:public' : userName}); - touchFields(['owner'], ModifyMethod.Unset); + {isUserAdmin && + + + { + setSharePublic(detail.checked); + setFields({owner: detail.checked ? 'lisa:public' : userName}); + setFields({groups: detail.checked ? [] : state.form.groups}); + touchFields(['owner'], ModifyMethod.Unset); + }} + disabled={disabled} /> + + + { + setFields({status: detail.checked ? McpServerStatus.Active : McpServerStatus.Inactive}); + touchFields(['status'], ModifyMethod.Unset); + }} + disabled={disabled} /> + + + + { + setTokenText(detail.value); + if (detail.value.length === 0) { + touchFields(['groups'], ModifyMethod.Unset); + } + }} onKeyDown={({detail}) => { + if (detail.keyCode === KeyCode.enter) { + setFields({groups: state.form.groups?.concat(`group:${tokenText}`) ?? [`group:${tokenText}`]}); + touchFields(['groups'], ModifyMethod.Unset); + setTokenText(''); + } }} - disabled={disabled} /> - - - { - setFields({status: detail.checked ? McpServerStatus.Active : McpServerStatus.Inactive}); - touchFields(['status'], ModifyMethod.Unset); + onBlur={() => { + if (tokenText.length === 0) { + touchFields(['groups'], ModifyMethod.Unset); + } else { + touchFields(['groups']); + } }} - disabled={disabled} /> + + placeholder='Enter group name' + disabled={disabled || sharePublic} /> + { + const newTokens = [...state.form.groups]; + newTokens.splice(detail.itemIndex, 1); + setFields({groups: newTokens}); + }} readOnly={disabled || sharePublic} /> - } + }
(undefined); @@ -171,11 +172,14 @@ export function McpServerManagementComponent () { pagination={} items={items} columnDefinitions={[ - { header: 'Use Server', cell: (item) => item.owner === userName || item.owner === 'lisa:public' ? server.id === item.id)?.enabled ?? false} onChange={({detail}) => toggleServer(item.id, item.name, detail.checked)}/> : <>}, + { header: 'Use Server', cell: (item) => item.owner === userName || item.owner === 'lisa:public' || item.groups?.map((group) => group.replace(/^\w+?:/, '')).filter((group) => userGroups.includes(group)).length > 0 ? server.id === item.id)?.enabled ?? false} onChange={({detail}) => toggleServer(item.id, item.name, detail.checked)}/> : <>}, { header: 'Name', cell: (item) => navigate(`./${item.id}`)}>{item.name}}, { header: 'Description', cell: (item) => item.description, id: 'description', sortingField: 'description'}, { header: 'URL', cell: (item) => item.url, id: 'url', sortingField: 'url'}, { header: 'Owner', cell: (item) => item.owner === 'lisa:public' ? (public) : item.owner, id: 'owner', sortingField: 'owner'}, + { header: 'Groups', cell: (item) => { + return item.groups?.length ? item.groups?.map((group) => group.replace(/^\w+?:/, '')).join(', ') : '-'; + }}, { header: 'Updated', cell: (item) => item.created, id: 'created', sortingField: 'created'}, ...(isUserAdmin ? [{ header: 'Status', cell: (item) => item.status ?? McpServerStatus.Inactive}] : []) ]} diff --git a/lib/user-interface/react/src/components/model-management/create-model/BaseModelConfig.tsx b/lib/user-interface/react/src/components/model-management/create-model/BaseModelConfig.tsx index a910c3e34..6f0ffe9b2 100644 --- a/lib/user-interface/react/src/components/model-management/create-model/BaseModelConfig.tsx +++ b/lib/user-interface/react/src/components/model-management/create-model/BaseModelConfig.tsx @@ -78,6 +78,11 @@ export function BaseModelConfig (props: FormProps & BaseModelConf props.setFields({ 'modelDescription': detail.value }); }} placeholder='Brief description of the model and its capabilities'/> + {!props.item.lisaHostedModel && API Key (optional)} errorText={props.formErrors?.apiKey}> + props.touchFields(['apiKey'])} onChange={({ detail }) => { + props.setFields({ 'apiKey': detail.value }); + }} disabled={props.isEdit}/> + } Model URL (optional)} errorText={props.formErrors?.modelUrl}> props.touchFields(['modelUrl'])} onChange={({ detail }) => { props.setFields({ 'modelUrl': detail.value }); diff --git a/lib/user-interface/react/src/pages/McpWorkbench.tsx b/lib/user-interface/react/src/pages/McpWorkbench.tsx new file mode 100644 index 000000000..f48e5d63d --- /dev/null +++ b/lib/user-interface/react/src/pages/McpWorkbench.tsx @@ -0,0 +1,33 @@ +/** + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { ReactElement, useEffect } from 'react'; +import { Route, Routes } from 'react-router-dom'; +import McpWorkbenchManagementComponent from '../components/mcp-workbench/McpWorkbenchManagementComponent'; + +export function McpWorkbench ({ setNav }): ReactElement { + useEffect(() => { + setNav(null); + }, [setNav]); + + return ( + + } /> + + ); +} + +export default McpWorkbench; diff --git a/lib/user-interface/react/src/shared/model/configuration.model.ts b/lib/user-interface/react/src/shared/model/configuration.model.ts index 5b365ccf8..c0c21660c 100644 --- a/lib/user-interface/react/src/shared/model/configuration.model.ts +++ b/lib/user-interface/react/src/shared/model/configuration.model.ts @@ -35,6 +35,7 @@ export type IEnabledComponents = { showPromptTemplateLibrary: boolean; enableModelComparisonUtility: boolean; mcpConnections: boolean; + showMcpWorkbench: boolean; modelLibrary: boolean; }; diff --git a/lib/user-interface/react/src/shared/model/mcp-tools.model.ts b/lib/user-interface/react/src/shared/model/mcp-tools.model.ts new file mode 100644 index 000000000..bc7fe4241 --- /dev/null +++ b/lib/user-interface/react/src/shared/model/mcp-tools.model.ts @@ -0,0 +1,70 @@ +/** + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +/** + * Interface representing an MCP Tool - TypeScript equivalent of Python MCPToolModel + */ +export type IMcpTool = { + /** The filename/toolId seen by frontend */ + id: string; + /** The Python code content */ + contents: string; + /** Timestamp of when the tool was created/updated */ + updated_at?: string; + /** File size in bytes (included in list responses) */ + size?: number; +}; + +/** + * Response interface for listing MCP tools + */ +export type IMcpToolListResponse = { + tools: IMcpTool[]; +}; + +/** + * Request interface for creating a new MCP tool + */ +export type IMcpToolRequest = { + /** The tool identifier/filename */ + id: string; + /** The Python code content */ + contents: string; +}; + +/** + * Request interface for updating an existing MCP tool + */ +export type IMcpToolUpdateRequest = { + /** The Python code content */ + contents: string; +}; + +/** + * Response interface for delete operations + */ +export type IMcpToolDeleteResponse = { + status: string; + message: string; +}; + +/** + * Default empty MCP tool for forms + */ +export const DefaultMcpTool: IMcpToolRequest = { + id: '', + contents: '' +}; diff --git a/lib/user-interface/react/src/shared/model/model-management.model.ts b/lib/user-interface/react/src/shared/model/model-management.model.ts index 61a42273e..ed33cae2b 100644 --- a/lib/user-interface/react/src/shared/model/model-management.model.ts +++ b/lib/user-interface/react/src/shared/model/model-management.model.ts @@ -128,6 +128,7 @@ export type IModelRequest = { loadBalancerConfig: ILoadBalancerConfig; lisaHostedModel: boolean; allowedGroups?: string[]; + apiKey?: string; }; export type ModelFeature = { diff --git a/lib/user-interface/react/src/shared/reducers/index.ts b/lib/user-interface/react/src/shared/reducers/index.ts index 665f04194..5b3852f51 100644 --- a/lib/user-interface/react/src/shared/reducers/index.ts +++ b/lib/user-interface/react/src/shared/reducers/index.ts @@ -26,6 +26,7 @@ import breadcrumbGroup from './breadcrumbs.reducer'; import { ragApi } from './rag.reducer'; import { promptTemplateApi } from './prompt-templates.reducer'; import { mcpServerApi } from '@/shared/reducers/mcp-server.reducer'; +import { mcpToolsApi } from '@/shared/reducers/mcp-tools.reducer'; import { userPreferencesApi } from '@/shared/reducers/user-preferences.reducer'; const rootReducer: ReducersMapObject = { @@ -39,9 +40,10 @@ const rootReducer: ReducersMapObject = { [ragApi.reducerPath]: ragApi.reducer, [promptTemplateApi.reducerPath]: promptTemplateApi.reducer, [mcpServerApi.reducerPath]: mcpServerApi.reducer, + [mcpToolsApi.reducerPath]: mcpToolsApi.reducer, [userPreferencesApi.reducerPath]: userPreferencesApi.reducer, }; -export const rootMiddleware = [modelManagementApi.middleware, configurationApi.middleware, sessionApi.middleware, ragApi.middleware, promptTemplateApi.middleware, mcpServerApi.middleware, userPreferencesApi.middleware, userPreferencesApi.middleware]; +export const rootMiddleware = [modelManagementApi.middleware, configurationApi.middleware, sessionApi.middleware, ragApi.middleware, promptTemplateApi.middleware, mcpServerApi.middleware, mcpToolsApi.middleware, userPreferencesApi.middleware]; export default rootReducer; diff --git a/lib/user-interface/react/src/shared/reducers/mcp-server.reducer.ts b/lib/user-interface/react/src/shared/reducers/mcp-server.reducer.ts index b6549d242..e0a0f95cc 100644 --- a/lib/user-interface/react/src/shared/reducers/mcp-server.reducer.ts +++ b/lib/user-interface/react/src/shared/reducers/mcp-server.reducer.ts @@ -34,6 +34,7 @@ export type McpServer = { url: string; name: string; description?: string; + groups?: string[]; isOwner?: true; customHeaders?: Record; clientConfig?: McpClientConfig; @@ -47,7 +48,8 @@ export const DefaultMcpServer: NewMcpServer = { url: '', description: '', clientConfig: {}, - status: McpServerStatus.Active + status: McpServerStatus.Active, + groups: [] }; export type McpServerListResponse = { @@ -70,10 +72,15 @@ export const mcpServerApi = createApi({ transformErrorResponse: (baseQueryReturnValue) => normalizeError('Create MCP Server', baseQueryReturnValue), invalidatesTags: ['mcpServers'], }), - getMcpServer: builder.query({ - query (serverId) { + getMcpServer: builder.query({ + query ({mcpServerId, showPlaceholder = false}) { + const queryStringParameters = new URLSearchParams(); + if (showPlaceholder) { + queryStringParameters.append('showPlaceholder', '1'); + } + return { - url: `/mcp-server/${serverId}`, + url: `/mcp-server/${mcpServerId}?${queryStringParameters.toString()}`, method: 'GET' }; }, diff --git a/lib/user-interface/react/src/shared/reducers/mcp-tools.reducer.ts b/lib/user-interface/react/src/shared/reducers/mcp-tools.reducer.ts new file mode 100644 index 000000000..5d1c94410 --- /dev/null +++ b/lib/user-interface/react/src/shared/reducers/mcp-tools.reducer.ts @@ -0,0 +1,86 @@ +/** + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { createApi } from '@reduxjs/toolkit/query/react'; +import { lisaBaseQuery } from './reducer.utils'; +import { normalizeError } from '../util/validationUtils'; +import { + IMcpTool, + IMcpToolListResponse, + IMcpToolRequest, + IMcpToolUpdateRequest, + IMcpToolDeleteResponse +} from '../model/mcp-tools.model'; + +export const mcpToolsApi = createApi({ + reducerPath: 'mcpTools', + baseQuery: lisaBaseQuery(), + tagTypes: ['mcpTools'], + refetchOnFocus: true, + refetchOnReconnect: true, + endpoints: (builder) => ({ + listMcpTools: builder.query({ + query: () => ({ + url: '/mcp-workbench', + method: 'GET' + }), + transformResponse: (response: IMcpToolListResponse) => response.tools, + providesTags: ['mcpTools'], + }), + getMcpTool: builder.query({ + query: (toolId) => ({ + url: `/mcp-workbench/${toolId}`, + method: 'GET' + }), + providesTags: ['mcpTools'], + }), + createMcpTool: builder.mutation({ + query: (mcpTool) => ({ + url: '/mcp-workbench', + method: 'POST', + data: mcpTool + }), + transformErrorResponse: (baseQueryReturnValue) => normalizeError('Create MCP Tool', baseQueryReturnValue), + invalidatesTags: ['mcpTools'], + }), + updateMcpTool: builder.mutation({ + query: ({ toolId, tool }) => ({ + url: `/mcp-workbench/${toolId}`, + method: 'PUT', + data: tool + }), + transformErrorResponse: (baseQueryReturnValue) => normalizeError('Update MCP Tool', baseQueryReturnValue), + invalidatesTags: ['mcpTools'], + }), + deleteMcpTool: builder.mutation({ + query: (toolId) => ({ + url: `/mcp-workbench/${toolId}`, + method: 'DELETE' + }), + transformErrorResponse: (baseQueryReturnValue) => normalizeError('Delete MCP Tool', baseQueryReturnValue), + invalidatesTags: ['mcpTools'], + }) + }), +}); + +export const { + useListMcpToolsQuery, + useGetMcpToolQuery, + useLazyGetMcpToolQuery, + useCreateMcpToolMutation, + useUpdateMcpToolMutation, + useDeleteMcpToolMutation +} = mcpToolsApi; diff --git a/lib/user-interface/react/src/shared/reducers/user.reducer.ts b/lib/user-interface/react/src/shared/reducers/user.reducer.ts index 1bc691937..6ff9b12ec 100644 --- a/lib/user-interface/react/src/shared/reducers/user.reducer.ts +++ b/lib/user-interface/react/src/shared/reducers/user.reducer.ts @@ -34,6 +34,7 @@ export const User = createSlice({ export const selectCurrentUserIsAdmin = (state: any) => state.user.info?.isAdmin ?? false; export const selectCurrentUsername = (state: any) => state.user.info?.preferred_username ?? ''; +export const selectCurrentUserGroups = (state: any) => state.user.info?.groups ?? []; export const { updateUserState } = User.actions; diff --git a/lib/util/common-functions.ts b/lib/util/common-functions.ts new file mode 100644 index 000000000..527081b4d --- /dev/null +++ b/lib/util/common-functions.ts @@ -0,0 +1,61 @@ +/** + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/** + * Executes a function on a value only if the value is defined (not null or undefined). + * This utility function provides a safe way to perform operations on potentially nullable values, + * avoiding the need for explicit null checks. + * + * Inspired by Kotlin's `let` scope function, which allows you to execute a block of code + * only when the receiver object is not null. This TypeScript implementation provides + * similar functionality for handling nullable values in a functional programming style. + * + * @template T - The type of the input value + * @template R - The type of the return value from the function + * @param value - The value to check and potentially transform (can be null or undefined) + * @param fn - The function to execute if the value is defined + * @returns The result of the function if value is defined, otherwise undefined + * + * @example + * // Basic usage with a string + * const name: string | null = getUserName(); + * const upperName = letIfDefined(name, (n) => n.toUpperCase()); + * // upperName will be the uppercase name if name exists, or undefined if name is null + * + * @example + * // Chaining operations safely + * const user: User | undefined = getUser(); + * const userEmail = letIfDefined(user, (u) => u.email?.toLowerCase()); + * + * @example + * // Using with complex transformations + * const config: Config | null = loadConfig(); + * const port = letIfDefined(config, (c) => c.server?.port ?? 3000); + * + * @example + * // Avoiding nested if statements + * // Instead of: + * // if (data != null) { + * // return processData(data); + * // } + * // return undefined; + * + * // Use: + * return letIfDefined(data, processData); + */ +export function letIfDefined (value: T | null | undefined, fn: (value: T) => R): R | undefined { + return value != null ? fn(value) : undefined; +} diff --git a/lib/util/paths.ts b/lib/util/paths.ts index 0827ad38b..545da4397 100644 --- a/lib/util/paths.ts +++ b/lib/util/paths.ts @@ -28,6 +28,7 @@ export const RAG_LAYER_PATH = path.join(ROOT_PATH, 'lib', 'rag', 'layer'); export const REST_API_PATH = path.join(ROOT_PATH, 'lib', 'serve', 'rest-api'); export const ECS_MODEL_PATH = path.join(ROOT_PATH, 'lib', 'serve', 'ecs-model'); +export const MCP_WORKBENCH_PATH = path.join(ROOT_PATH, 'lib', 'serve', 'mcp-workbench'); export const BATCH_INGESTION_PATH = path.join(ROOT_PATH, 'lib', 'rag', 'ingestion', 'ingestion-image'); export const WEBAPP_PATH = path.join(ROOT_PATH, 'lib', 'user-interface', 'react'); diff --git a/lisa-sdk/pyproject.toml b/lisa-sdk/pyproject.toml index 8920fc056..1240fdeb4 100644 --- a/lisa-sdk/pyproject.toml +++ b/lisa-sdk/pyproject.toml @@ -3,7 +3,7 @@ requires-python = ">=3.11" [tool.poetry] name = "lisapy" -version = "5.2.0" +version = "5.3.0" description = "A simple SDK to help you interact with LISA. LISA is an LLM hosting solution for AWS dedicated clouds or ADCs." authors = ["Steve Goley "] readme = "README.md" diff --git a/lisa-sdk/tutorial_01.ipynb b/lisa-sdk/tutorial_01.ipynb index e6aac86cd..79c9d215f 100644 --- a/lisa-sdk/tutorial_01.ipynb +++ b/lisa-sdk/tutorial_01.ipynb @@ -244,7 +244,6 @@ "\n", "# from langchain_community.vectorstores import OpenSearchVectorSearch\n", "\n", - "from lisapy import Lisa\n", "from lisapy.langchain import LisaTextgen\n", "from lisapy.langchain import LisaEmbeddings\n", "from lisapy.authentication import get_cognito_token\n", diff --git a/package-lock.json b/package-lock.json index b4f44d701..be32efd64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@awslabs/lisa", - "version": "5.2.0", + "version": "5.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@awslabs/lisa", - "version": "5.2.0", + "version": "5.3.0", "license": "Apache-2.0", "workspaces": [ "lib/user-interface/react", @@ -100,12 +100,12 @@ }, "lib/user-interface/react": { "name": "lisa-web", - "version": "5.2.0", + "version": "5.3.0", "dependencies": { - "@cloudscape-design/chat-components": "^1.0.22", - "@cloudscape-design/collection-hooks": "^1.0.59", - "@cloudscape-design/component-toolkit": "^1.0.0-beta.65", - "@cloudscape-design/components": "^3.0.883", + "@cloudscape-design/chat-components": "^1.0.61", + "@cloudscape-design/collection-hooks": "^1.0.74", + "@cloudscape-design/component-toolkit": "^1.0.0-beta.114", + "@cloudscape-design/components": "^3.0.1071", "@cloudscape-design/global-styles": "^1.0.35", "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-regular-svg-icons": "^6.7.2", @@ -116,6 +116,7 @@ "@microsoft/fetch-event-source": "^2.0.1", "@reduxjs/toolkit": "^1.9.7", "@swc/core": "^1.11.8", + "ace-builds": "^1.43.2", "axios": "^1.8.2", "git-repo-info": "^2.1.1", "jszip": "^3.10.1", @@ -124,6 +125,7 @@ "luxon": "^3.5.0", "mermaid": "^11.10.1", "react": "^18.3.1", + "react-ace": "^14.0.1", "react-dom": "^18.3.1", "react-json-view-lite": "^0.9.8", "react-markdown": "^9.0.3", @@ -144,6 +146,7 @@ "vitepress": "^1.6.4" }, "devDependencies": { + "@types/ace": "^0.0.52", "@types/markdown-it": "^14.1.2", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", @@ -171,8 +174,6 @@ }, "node_modules/@algolia/abtesting": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.1.0.tgz", - "integrity": "sha512-sEyWjw28a/9iluA37KLGu8vjxEIlb60uxznfTUmXImy7H5NvbpSO6yYgmgH5KiD7j+zTUUihiST0jEP12IoXow==", "license": "MIT", "dependencies": { "@algolia/client-common": "5.35.0", @@ -186,8 +187,6 @@ }, "node_modules/@algolia/autocomplete-core": { "version": "1.17.7", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", - "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", "license": "MIT", "dependencies": { "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", @@ -196,8 +195,6 @@ }, "node_modules/@algolia/autocomplete-plugin-algolia-insights": { "version": "1.17.7", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz", - "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", "license": "MIT", "dependencies": { "@algolia/autocomplete-shared": "1.17.7" @@ -208,8 +205,6 @@ }, "node_modules/@algolia/autocomplete-preset-algolia": { "version": "1.17.7", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz", - "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", "license": "MIT", "dependencies": { "@algolia/autocomplete-shared": "1.17.7" @@ -221,8 +216,6 @@ }, "node_modules/@algolia/autocomplete-shared": { "version": "1.17.7", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz", - "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", "license": "MIT", "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", @@ -231,8 +224,6 @@ }, "node_modules/@algolia/client-abtesting": { "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.35.0.tgz", - "integrity": "sha512-uUdHxbfHdoppDVflCHMxRlj49/IllPwwQ2cQ8DLC4LXr3kY96AHBpW0dMyi6ygkn2MtFCc6BxXCzr668ZRhLBQ==", "license": "MIT", "dependencies": { "@algolia/client-common": "5.35.0", @@ -246,8 +237,6 @@ }, "node_modules/@algolia/client-analytics": { "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.35.0.tgz", - "integrity": "sha512-SunAgwa9CamLcRCPnPHx1V2uxdQwJGqb1crYrRWktWUdld0+B2KyakNEeVn5lln4VyeNtW17Ia7V7qBWyM/Skw==", "license": "MIT", "dependencies": { "@algolia/client-common": "5.35.0", @@ -261,8 +250,6 @@ }, "node_modules/@algolia/client-common": { "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.35.0.tgz", - "integrity": "sha512-ipE0IuvHu/bg7TjT2s+187kz/E3h5ssfTtjpg1LbWMgxlgiaZIgTTbyynM7NfpSJSKsgQvCQxWjGUO51WSCu7w==", "license": "MIT", "engines": { "node": ">= 14.0.0" @@ -270,8 +257,6 @@ }, "node_modules/@algolia/client-insights": { "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.35.0.tgz", - "integrity": "sha512-UNbCXcBpqtzUucxExwTSfAe8gknAJ485NfPN6o1ziHm6nnxx97piIbcBQ3edw823Tej2Wxu1C0xBY06KgeZ7gA==", "license": "MIT", "dependencies": { "@algolia/client-common": "5.35.0", @@ -285,8 +270,6 @@ }, "node_modules/@algolia/client-personalization": { "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.35.0.tgz", - "integrity": "sha512-/KWjttZ6UCStt4QnWoDAJ12cKlQ+fkpMtyPmBgSS2WThJQdSV/4UWcqCUqGH7YLbwlj3JjNirCu3Y7uRTClxvA==", "license": "MIT", "dependencies": { "@algolia/client-common": "5.35.0", @@ -300,8 +283,6 @@ }, "node_modules/@algolia/client-query-suggestions": { "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.35.0.tgz", - "integrity": "sha512-8oCuJCFf/71IYyvQQC+iu4kgViTODbXDk3m7yMctEncRSRV+u2RtDVlpGGfPlJQOrAY7OONwJlSHkmbbm2Kp/w==", "license": "MIT", "dependencies": { "@algolia/client-common": "5.35.0", @@ -315,8 +296,6 @@ }, "node_modules/@algolia/client-search": { "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.35.0.tgz", - "integrity": "sha512-FfmdHTrXhIduWyyuko1YTcGLuicVbhUyRjO3HbXE4aP655yKZgdTIfMhZ/V5VY9bHuxv/fGEh3Od1Lvv2ODNTg==", "license": "MIT", "dependencies": { "@algolia/client-common": "5.35.0", @@ -330,8 +309,6 @@ }, "node_modules/@algolia/ingestion": { "version": "1.35.0", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.35.0.tgz", - "integrity": "sha512-gPzACem9IL1Co8mM1LKMhzn1aSJmp+Vp434An4C0OBY4uEJRcqsLN3uLBlY+bYvFg8C8ImwM9YRiKczJXRk0XA==", "license": "MIT", "dependencies": { "@algolia/client-common": "5.35.0", @@ -345,8 +322,6 @@ }, "node_modules/@algolia/monitoring": { "version": "1.35.0", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.35.0.tgz", - "integrity": "sha512-w9MGFLB6ashI8BGcQoVt7iLgDIJNCn4OIu0Q0giE3M2ItNrssvb8C0xuwJQyTy1OFZnemG0EB1OvXhIHOvQwWw==", "license": "MIT", "dependencies": { "@algolia/client-common": "5.35.0", @@ -360,8 +335,6 @@ }, "node_modules/@algolia/recommend": { "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.35.0.tgz", - "integrity": "sha512-AhrVgaaXAb8Ue0u2nuRWwugt0dL5UmRgS9LXe0Hhz493a8KFeZVUE56RGIV3hAa6tHzmAV7eIoqcWTQvxzlJeQ==", "license": "MIT", "dependencies": { "@algolia/client-common": "5.35.0", @@ -375,8 +348,6 @@ }, "node_modules/@algolia/requester-browser-xhr": { "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.35.0.tgz", - "integrity": "sha512-diY415KLJZ6x1Kbwl9u96Jsz0OstE3asjXtJ9pmk1d+5gPuQ5jQyEsgC+WmEXzlec3iuVszm8AzNYYaqw6B+Zw==", "license": "MIT", "dependencies": { "@algolia/client-common": "5.35.0" @@ -387,8 +358,6 @@ }, "node_modules/@algolia/requester-fetch": { "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.35.0.tgz", - "integrity": "sha512-uydqnSmpAjrgo8bqhE9N1wgcB98psTRRQXcjc4izwMB7yRl9C8uuAQ/5YqRj04U0mMQ+fdu2fcNF6m9+Z1BzDQ==", "license": "MIT", "dependencies": { "@algolia/client-common": "5.35.0" @@ -399,8 +368,6 @@ }, "node_modules/@algolia/requester-node-http": { "version": "5.35.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.35.0.tgz", - "integrity": "sha512-RgLX78ojYOrThJHrIiPzT4HW3yfQa0D7K+MQ81rhxqaNyNBu4F1r+72LNHYH/Z+y9I1Mrjrd/c/Ue5zfDgAEjQ==", "license": "MIT", "dependencies": { "@algolia/client-common": "5.35.0" @@ -433,8 +400,6 @@ }, "node_modules/@antfu/install-pkg": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", - "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", "license": "MIT", "dependencies": { "package-manager-detector": "^1.3.0", @@ -445,9 +410,7 @@ } }, "node_modules/@antfu/utils": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz", - "integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==", + "version": "9.2.0", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" @@ -455,8 +418,6 @@ }, "node_modules/@aws-cdk/asset-awscli-v1": { "version": "2.2.230", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.230.tgz", - "integrity": "sha512-kUnhKIYu42hqBa6a8x2/7o29ObpJgjYGQy28lZDq9awXyvpR62I2bRxrNKNR3uFUQz3ySuT9JXhGHhuZPdbnFw==", "license": "Apache-2.0" }, "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { @@ -477,8 +438,6 @@ }, "node_modules/@aws-cdk/cloud-assembly-schema": { "version": "41.2.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-41.2.0.tgz", - "integrity": "sha512-JaulVS6z9y5+u4jNmoWbHZRs9uGOnmn/ktXygNWKNu1k6lF3ad4so3s18eRu15XCbUIomxN9WPYT6Ehh7hzONw==", "bundleDependencies": [ "jsonschema", "semver" @@ -1243,8 +1202,6 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1252,8 +1209,6 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1269,8 +1224,6 @@ }, "node_modules/@babel/helpers": { "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", - "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", "dev": true, "license": "MIT", "dependencies": { @@ -1283,8 +1236,6 @@ }, "node_modules/@babel/parser": { "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", - "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", "license": "MIT", "dependencies": { "@babel/types": "^7.27.3" @@ -1503,8 +1454,6 @@ }, "node_modules/@babel/runtime": { "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", - "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" @@ -1515,8 +1464,6 @@ }, "node_modules/@babel/template": { "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", "dev": true, "license": "MIT", "dependencies": { @@ -1547,8 +1494,6 @@ }, "node_modules/@babel/traverse/node_modules/globals": { "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, "license": "MIT", "engines": { @@ -1557,8 +1502,6 @@ }, "node_modules/@babel/types": { "version": "7.27.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", - "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -1575,8 +1518,6 @@ }, "node_modules/@braintree/sanitize-url": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", - "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==", "license": "MIT" }, "node_modules/@cdklabs/cdk-enterprise-iac": { @@ -2076,8 +2017,6 @@ }, "node_modules/@chevrotain/cst-dts-gen": { "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", - "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", "license": "Apache-2.0", "dependencies": { "@chevrotain/gast": "11.0.3", @@ -2087,8 +2026,6 @@ }, "node_modules/@chevrotain/gast": { "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", - "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", "license": "Apache-2.0", "dependencies": { "@chevrotain/types": "11.0.3", @@ -2097,24 +2034,18 @@ }, "node_modules/@chevrotain/regexp-to-ast": { "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", - "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", "license": "Apache-2.0" }, "node_modules/@chevrotain/types": { "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", - "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", "license": "Apache-2.0" }, "node_modules/@chevrotain/utils": { "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", - "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", "license": "Apache-2.0" }, "node_modules/@cloudscape-design/chat-components": { - "version": "1.0.22", + "version": "1.0.61", "license": "Apache-2.0", "dependencies": { "@cloudscape-design/component-toolkit": "^1.0.0-beta", @@ -2123,20 +2054,18 @@ }, "peerDependencies": { "@cloudscape-design/components": "^3", - "@cloudscape-design/design-tokens": "^3", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "react": ">=18.2.0" } }, "node_modules/@cloudscape-design/collection-hooks": { - "version": "1.0.61", + "version": "1.0.74", "license": "Apache-2.0", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": ">=16.8.0" } }, "node_modules/@cloudscape-design/component-toolkit": { - "version": "1.0.0-beta.88", + "version": "1.0.0-beta.117", "license": "Apache-2.0", "dependencies": { "@juggle/resize-observer": "^3.3.1", @@ -2144,7 +2073,7 @@ } }, "node_modules/@cloudscape-design/components": { - "version": "3.0.895", + "version": "3.0.1077", "license": "Apache-2.0", "dependencies": { "@cloudscape-design/collection-hooks": "^1.0.0", @@ -2168,15 +2097,9 @@ "weekstart": "^1.1.0" }, "peerDependencies": { - "react": "^16.8 || ^17 || ^18", - "react-dom": "^16.8 || ^17 || ^18" + "react": ">=16.8.0" } }, - "node_modules/@cloudscape-design/design-tokens": { - "version": "3.0.51", - "license": "Apache-2.0", - "peer": true - }, "node_modules/@cloudscape-design/global-styles": { "version": "1.0.36", "license": "Apache-2.0" @@ -2198,9 +2121,8 @@ }, "node_modules/@colors/colors": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">=0.1.90" @@ -2228,9 +2150,8 @@ }, "node_modules/@cypress/request": { "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.8.tgz", - "integrity": "sha512-h0NFgh1mJmm1nr4jCwkGHwKneVYKghUyWe6TMNrk0B9zsjAJxpg8C4/+BAcmLgCPa1vj1V8rNUaILl+zYRUWBQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -2257,18 +2178,16 @@ }, "node_modules/@cypress/request/node_modules/uuid": { "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true, + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } }, "node_modules/@cypress/xvfb": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", - "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^3.1.0", "lodash.once": "^4.1.1" @@ -2276,9 +2195,8 @@ }, "node_modules/@cypress/xvfb/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } @@ -2330,14 +2248,10 @@ }, "node_modules/@docsearch/css": { "version": "3.8.2", - "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", - "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==", "license": "MIT" }, "node_modules/@docsearch/js": { "version": "3.8.2", - "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz", - "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==", "license": "MIT", "dependencies": { "@docsearch/react": "3.8.2", @@ -2346,8 +2260,6 @@ }, "node_modules/@docsearch/react": { "version": "3.8.2", - "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz", - "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==", "license": "MIT", "dependencies": { "@algolia/autocomplete-core": "1.17.7", @@ -2376,468 +2288,275 @@ } } }, - "node_modules/@esbuild/aix-ppc64": { + "node_modules/@esbuild/linux-x64": { "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", "cpu": [ - "ppc64" + "x64" ], "license": "MIT", "optional": true, "os": [ - "aix" + "linux" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", - "cpu": [ - "arm" - ], + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": ">=18" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", - "cpu": [ - "x64" - ], + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=18" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", - "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, "engines": { - "node": ">=18" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "node_modules/@eslint/eslintrc/node_modules/espree": { + "version": "9.6.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, "engines": { - "node": ">=18" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", - "cpu": [ - "arm" - ], + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "type-fest": "^0.20.2" + }, "engines": { - "node": ">=18" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, "engines": { - "node": ">=18" + "node": "*" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=18" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", - "cpu": [ - "loong64" - ], + "node_modules/@eslint/js": { + "version": "8.57.1", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", - "cpu": [ - "mips64el" - ], + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.3", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@formatjs/fast-memoize": "2.2.6", + "@formatjs/intl-localematcher": "0.6.0", + "decimal.js": "10", + "tslib": "2" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.6", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "2" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", - "cpu": [ - "riscv64" - ], + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.1", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.3", + "@formatjs/icu-skeleton-parser": "1.8.13", + "tslib": "2" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", - "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", - "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", - "cpu": [ - "arm64" - ], + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.13", "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.3", + "tslib": "2" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", - "cpu": [ - "x64" - ], + "node_modules/@formatjs/intl-localematcher": { + "version": "0.6.0", "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "2" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", - "cpu": [ - "x64" - ], + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.7.2", "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], "engines": { - "node": ">=18" + "node": ">=6" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", - "cpu": [ - "arm64" - ], + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.7.2", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, "engines": { - "node": ">=18" + "node": ">=6" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "6.7.2", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, "engines": { - "node": ">=18" + "node": ">=6" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.7.2", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.7.2" + }, "engines": { - "node": ">=18" + "node": ">=6" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "dev": true, + "node_modules/@fortawesome/react-fontawesome": { + "version": "0.2.2", "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "prop-types": "^15.8.1" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "react": ">=16.3" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", + "node_modules/@hapi/hoek": { + "version": "9.3.0", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } + "license": "BSD-3-Clause" }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", + "node_modules/@hapi/topo": { + "version": "5.1.0", "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=10.10.0" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2845,281 +2564,64 @@ "concat-map": "0.0.1" } }, - "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { - "version": "3.4.3", + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" }, - "funding": { - "url": "https://opencollective.com/eslint" + "engines": { + "node": "*" } }, - "node_modules/@eslint/eslintrc/node_modules/espree": { - "version": "9.6.1", + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, + "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=12.22" }, "funding": { - "url": "https://opencollective.com/eslint" + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.48", + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.0.1", "license": "MIT", "dependencies": { - "type-fest": "^0.20.2" - }, + "@antfu/install-pkg": "^1.1.0", + "@antfu/utils": "^9.2.0", + "@iconify/types": "^2.0.0", + "debug": "^4.4.1", + "globals": "^15.15.0", + "kolorist": "^1.8.0", + "local-pkg": "^1.1.1", + "mlly": "^1.7.4" + } + }, + "node_modules/@iconify/utils/node_modules/globals": { + "version": "15.15.0", + "license": "MIT", "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@formatjs/ecma402-abstract": { - "version": "2.3.3", - "license": "MIT", - "dependencies": { - "@formatjs/fast-memoize": "2.2.6", - "@formatjs/intl-localematcher": "0.6.0", - "decimal.js": "10", - "tslib": "2" - } - }, - "node_modules/@formatjs/fast-memoize": { - "version": "2.2.6", - "license": "MIT", - "dependencies": { - "tslib": "2" - } - }, - "node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.11.1", - "license": "MIT", - "dependencies": { - "@formatjs/ecma402-abstract": "2.3.3", - "@formatjs/icu-skeleton-parser": "1.8.13", - "tslib": "2" - } - }, - "node_modules/@formatjs/icu-skeleton-parser": { - "version": "1.8.13", - "license": "MIT", - "dependencies": { - "@formatjs/ecma402-abstract": "2.3.3", - "tslib": "2" - } - }, - "node_modules/@formatjs/intl-localematcher": { - "version": "0.6.0", - "license": "MIT", - "dependencies": { - "tslib": "2" - } - }, - "node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz", - "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz", - "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==", - "license": "MIT", - "dependencies": { - "@fortawesome/fontawesome-common-types": "6.7.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/free-regular-svg-icons": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz", - "integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==", - "license": "(CC-BY-4.0 AND MIT)", - "dependencies": { - "@fortawesome/fontawesome-common-types": "6.7.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz", - "integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==", - "license": "(CC-BY-4.0 AND MIT)", - "dependencies": { - "@fortawesome/fontawesome-common-types": "6.7.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@fortawesome/react-fontawesome": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz", - "integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==", - "license": "MIT", - "dependencies": { - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "@fortawesome/fontawesome-svg-core": "~1 || ~6", - "react": ">=16.3" - } - }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "dev": true - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "dev": true, - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@iconify-json/simple-icons": { - "version": "1.2.48", - "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.48.tgz", - "integrity": "sha512-EACOtZMoPJtERiAbX1De0asrrCtlwI27+03c9OJlYWsly9w1O5vcD8rTzh+kDPjo+K8FOVnq2Qy+h/CzljSKDA==", - "license": "CC0-1.0", - "dependencies": { - "@iconify/types": "*" - } - }, - "node_modules/@iconify/types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", - "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", - "license": "MIT" - }, - "node_modules/@iconify/utils": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz", - "integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==", - "license": "MIT", - "dependencies": { - "@antfu/install-pkg": "^1.0.0", - "@antfu/utils": "^8.1.0", - "@iconify/types": "^2.0.0", - "debug": "^4.4.0", - "globals": "^15.14.0", - "kolorist": "^1.8.0", - "local-pkg": "^1.0.0", - "mlly": "^1.7.4" - } - }, - "node_modules/@iconify/utils/node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", - "license": "MIT", - "engines": { - "node": ">=18" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -3598,8 +3100,6 @@ }, "node_modules/@jridgewell/source-map": { "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "license": "MIT", "optional": true, "peer": true, @@ -3697,8 +3197,6 @@ }, "node_modules/@mermaid-js/parser": { "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.2.tgz", - "integrity": "sha512-+PO02uGF6L6Cs0Bw8RpGhikVvMWEysfAyl27qTlroUB8jSWr1lL0Sf6zi78ZxlSnmgSY2AMMKVgghnN9jTtwkQ==", "license": "MIT", "dependencies": { "langium": "3.3.1" @@ -3710,8 +3208,6 @@ }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.13.3", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.13.3.tgz", - "integrity": "sha512-bGwA78F/U5G2jrnsdRkPY3IwIwZeWUEfb5o764b79lb0rJmMT76TLwKhdNZOWakOQtedYefwIR4emisEMvInKA==", "license": "MIT", "dependencies": { "ajv": "^6.12.6", @@ -3808,287 +3304,56 @@ "node": ">=14.0.0" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { + "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.9.tgz", - "integrity": "sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA==", "cpu": [ - "arm" + "x64" ], "license": "MIT", "optional": true, "os": [ - "android" + "linux" ] }, - "node_modules/@rollup/rollup-android-arm64": { + "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.9.tgz", - "integrity": "sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==", "cpu": [ - "arm64" + "x64" ], "license": "MIT", "optional": true, "os": [ - "android" + "linux" ] }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz", - "integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==", - "cpu": [ - "arm64" - ], + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@shikijs/core": { + "version": "2.5.0", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "dependencies": { + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz", - "integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==", - "cpu": [ - "x64" - ], + "node_modules/@shikijs/engine-javascript": { + "version": "2.5.0", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^3.1.0" + } }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.9.tgz", - "integrity": "sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.9.tgz", - "integrity": "sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.9.tgz", - "integrity": "sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.9.tgz", - "integrity": "sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz", - "integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz", - "integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.9.tgz", - "integrity": "sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.9.tgz", - "integrity": "sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.9.tgz", - "integrity": "sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.9.tgz", - "integrity": "sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz", - "integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz", - "integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz", - "integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.9.tgz", - "integrity": "sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.34.9", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz", - "integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@shikijs/core": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", - "integrity": "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==", - "license": "MIT", - "dependencies": { - "@shikijs/engine-javascript": "2.5.0", - "@shikijs/engine-oniguruma": "2.5.0", - "@shikijs/types": "2.5.0", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4", - "hast-util-to-html": "^9.0.4" - } - }, - "node_modules/@shikijs/engine-javascript": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz", - "integrity": "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "2.5.0", - "@shikijs/vscode-textmate": "^10.0.2", - "oniguruma-to-es": "^3.1.0" - } - }, - "node_modules/@shikijs/engine-oniguruma": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz", - "integrity": "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==", + "node_modules/@shikijs/engine-oniguruma": { + "version": "2.5.0", "license": "MIT", "dependencies": { "@shikijs/types": "2.5.0", @@ -4097,8 +3362,6 @@ }, "node_modules/@shikijs/langs": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-2.5.0.tgz", - "integrity": "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==", "license": "MIT", "dependencies": { "@shikijs/types": "2.5.0" @@ -4106,8 +3369,6 @@ }, "node_modules/@shikijs/themes": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-2.5.0.tgz", - "integrity": "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==", "license": "MIT", "dependencies": { "@shikijs/types": "2.5.0" @@ -4115,8 +3376,6 @@ }, "node_modules/@shikijs/transformers": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz", - "integrity": "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==", "license": "MIT", "dependencies": { "@shikijs/core": "2.5.0", @@ -4125,8 +3384,6 @@ }, "node_modules/@shikijs/types": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", - "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", @@ -4135,30 +3392,25 @@ }, "node_modules/@shikijs/vscode-textmate": { "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", - "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "license": "MIT" }, "node_modules/@sideway/address": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@hapi/hoek": "^9.0.0" } }, "node_modules/@sideway/formula": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@sideway/pinpoint": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@sinclair/typebox": { "version": "0.27.8", @@ -4700,8 +3952,6 @@ }, "node_modules/@swc/core": { "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.11.tgz", - "integrity": "sha512-pCVY2Wn6dV/labNvssk9b3Owi4WOYsapcbWm90XkIj4xH/56Z6gzja9fsU+4MdPuEfC2Smw835nZHcdCFGyX6A==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -4736,90 +3986,8 @@ } } }, - "node_modules/@swc/core-darwin-arm64": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.11.tgz", - "integrity": "sha512-vJcjGVDB8cZH7zyOkC0AfpFYI/7GHKG0NSsH3tpuKrmoAXJyCYspKPGid7FT53EAlWreN7+Pew+bukYf5j+Fmg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-darwin-x64": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.11.tgz", - "integrity": "sha512-/N4dGdqEYvD48mCF3QBSycAbbQd3yoZ2YHSzYesQf8usNc2YpIhYqEH3sql02UsxTjEFOJSf1bxZABDdhbSl6A==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.11.tgz", - "integrity": "sha512-hsBhKK+wVXdN3x9MrL5GW0yT8o9GxteE5zHAI2HJjRQel3HtW7m5Nvwaq+q8rwMf4YQRd8ydbvwl4iUOZx7i2Q==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.11.tgz", - "integrity": "sha512-YOCdxsqbnn/HMPCNM6nrXUpSndLXMUssGTtzT7ffXqr7WuzRg2e170FVDVQFIkb08E7Ku5uOnnUVAChAJQbMOQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.11.tgz", - "integrity": "sha512-nR2tfdQRRzwqR2XYw9NnBk9Fdvff/b8IiJzDL28gRR2QiJWLaE8LsRovtWrzCOYq6o5Uu9cJ3WbabWthLo4jLw==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, "node_modules/@swc/core-linux-x64-gnu": { "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.11.tgz", - "integrity": "sha512-b4gBp5HA9xNWNC5gsYbdzGBJWx4vKSGybGMGOVWWuF+ynx10+0sA/o4XJGuNHm8TEDuNh9YLKf6QkIO8+GPJ1g==", "cpu": [ "x64" ], @@ -4834,8 +4002,6 @@ }, "node_modules/@swc/core-linux-x64-musl": { "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.11.tgz", - "integrity": "sha512-dEvqmQVswjNvMBwXNb8q5uSvhWrJLdttBSef3s6UC5oDSwOr00t3RQPzyS3n5qmGJ8UMTdPRmsopxmqaODISdg==", "cpu": [ "x64" ], @@ -4848,67 +4014,15 @@ "node": ">=10" } }, - "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.11.tgz", - "integrity": "sha512-aZNZznem9WRnw2FbTqVpnclvl8Q2apOBW2B316gZK+qxbe+ktjOUnYaMhdCG3+BYggyIBDOnaJeQrXbKIMmNdw==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } + "node_modules/@swc/counter": { + "version": "0.1.3", + "license": "Apache-2.0" }, - "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.11.tgz", - "integrity": "sha512-DjeJn/IfjgOddmJ8IBbWuDK53Fqw7UvOz7kyI/728CSdDYC3LXigzj3ZYs4VvyeOt+ZcQZUB2HA27edOifomGw==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.11.tgz", - "integrity": "sha512-Gp/SLoeMtsU4n0uRoKDOlGrRC6wCfifq7bqLwSlAG8u8MyJYJCcwjg7ggm0rhLdC2vbiZ+lLVl3kkETp+JUvKg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "license": "Apache-2.0" - }, - "node_modules/@swc/types": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.19.tgz", - "integrity": "sha512-WkAZaAfj44kh/UFdAQcrMP1I0nwRqpt27u+08LMBYMqmQfwwMofYoMh/48NGkMMRfC4ynpfwRbJuu8ErfNloeA==", - "license": "Apache-2.0", - "dependencies": { - "@swc/counter": "^0.1.3" + "node_modules/@swc/types": { + "version": "0.1.19", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" } }, "node_modules/@tsconfig/node10": { @@ -4931,6 +4045,11 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@types/ace": { + "version": "0.0.52", + "dev": true, + "license": "MIT" + }, "node_modules/@types/aws-lambda": { "version": "8.10.147", "dev": true, @@ -4975,8 +4094,6 @@ }, "node_modules/@types/d3": { "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", - "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", "license": "MIT", "dependencies": { "@types/d3-array": "*", @@ -5012,15 +4129,11 @@ } }, "node_modules/@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "version": "3.2.2", "license": "MIT" }, "node_modules/@types/d3-axis": { "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", - "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", "license": "MIT", "dependencies": { "@types/d3-selection": "*" @@ -5028,8 +4141,6 @@ }, "node_modules/@types/d3-brush": { "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", - "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", "license": "MIT", "dependencies": { "@types/d3-selection": "*" @@ -5037,20 +4148,14 @@ }, "node_modules/@types/d3-chord": { "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", - "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", "license": "MIT" }, "node_modules/@types/d3-color": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "license": "MIT" }, "node_modules/@types/d3-contour": { "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", - "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", "license": "MIT", "dependencies": { "@types/d3-array": "*", @@ -5059,20 +4164,14 @@ }, "node_modules/@types/d3-delaunay": { "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", "license": "MIT" }, "node_modules/@types/d3-dispatch": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", - "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", "license": "MIT" }, "node_modules/@types/d3-drag": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", - "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", "license": "MIT", "dependencies": { "@types/d3-selection": "*" @@ -5080,20 +4179,14 @@ }, "node_modules/@types/d3-dsv": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", - "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", "license": "MIT" }, "node_modules/@types/d3-ease": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", "license": "MIT" }, "node_modules/@types/d3-fetch": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", - "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", "license": "MIT", "dependencies": { "@types/d3-dsv": "*" @@ -5101,20 +4194,14 @@ }, "node_modules/@types/d3-force": { "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", - "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", "license": "MIT" }, "node_modules/@types/d3-format": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", - "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", "license": "MIT" }, "node_modules/@types/d3-geo": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", - "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", "license": "MIT", "dependencies": { "@types/geojson": "*" @@ -5122,14 +4209,10 @@ }, "node_modules/@types/d3-hierarchy": { "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", - "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", "license": "MIT" }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", "license": "MIT", "dependencies": { "@types/d3-color": "*" @@ -5137,32 +4220,22 @@ }, "node_modules/@types/d3-path": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", "license": "MIT" }, "node_modules/@types/d3-polygon": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", - "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", "license": "MIT" }, "node_modules/@types/d3-quadtree": { "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", - "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", "license": "MIT" }, "node_modules/@types/d3-random": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", - "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", "license": "MIT" }, "node_modules/@types/d3-scale": { "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", "license": "MIT", "dependencies": { "@types/d3-time": "*" @@ -5170,20 +4243,14 @@ }, "node_modules/@types/d3-scale-chromatic": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", "license": "MIT" }, "node_modules/@types/d3-selection": { "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", - "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", "license": "MIT" }, "node_modules/@types/d3-shape": { "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", "license": "MIT", "dependencies": { "@types/d3-path": "*" @@ -5191,26 +4258,18 @@ }, "node_modules/@types/d3-time": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", "license": "MIT" }, "node_modules/@types/d3-time-format": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", - "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", "license": "MIT" }, "node_modules/@types/d3-timer": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, "node_modules/@types/d3-transition": { "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", - "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", "license": "MIT", "dependencies": { "@types/d3-selection": "*" @@ -5218,8 +4277,6 @@ }, "node_modules/@types/d3-zoom": { "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", - "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", "license": "MIT", "dependencies": { "@types/d3-interpolate": "*", @@ -5251,8 +4308,6 @@ }, "node_modules/@types/geojson": { "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "license": "MIT" }, "node_modules/@types/graceful-fs": { @@ -5325,8 +4380,6 @@ }, "node_modules/@types/katex": { "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz", - "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", "license": "MIT" }, "node_modules/@types/linkify-it": { @@ -5348,8 +4401,6 @@ }, "node_modules/@types/mathjax": { "version": "0.0.40", - "resolved": "https://registry.npmjs.org/@types/mathjax/-/mathjax-0.0.40.tgz", - "integrity": "sha512-rHusx08LCg92WJxrsM3SPjvLTSvK5C+gealtSuhKbEOcUZfWlwigaFoPLf6Dfxhg4oryN5qP9Sj7zOQ4HYXINw==", "license": "MIT" }, "node_modules/@types/mdast": { @@ -5374,8 +4425,7 @@ }, "node_modules/@types/node": { "version": "22.14.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", - "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } @@ -5445,15 +4495,13 @@ }, "node_modules/@types/sinonjs__fake-timers": { "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", - "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/sizzle": { "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz", - "integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/stack-utils": { "version": "2.0.3", @@ -5462,8 +4510,6 @@ }, "node_modules/@types/trusted-types": { "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT", "optional": true }, @@ -5482,8 +4528,6 @@ }, "node_modules/@types/web-bluetooth": { "version": "0.0.21", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", - "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", "license": "MIT" }, "node_modules/@types/yargs": { @@ -5501,9 +4545,8 @@ }, "node_modules/@types/yauzl": { "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "@types/node": "*" @@ -5859,8 +4902,6 @@ }, "node_modules/@vitejs/plugin-vue": { "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", - "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", "license": "MIT", "engines": { "node": "^18.0.0 || >=20.0.0" @@ -5872,8 +4913,6 @@ }, "node_modules/@vue/compiler-core": { "version": "3.5.16", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.16.tgz", - "integrity": "sha512-AOQS2eaQOaaZQoL1u+2rCJIKDruNXVBZSiUD3chnUrsoX5ZTQMaCvXlWNIfxBJuU15r1o7+mpo5223KVtIhAgQ==", "license": "MIT", "dependencies": { "@babel/parser": "^7.27.2", @@ -5885,8 +4924,6 @@ }, "node_modules/@vue/compiler-dom": { "version": "3.5.16", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.16.tgz", - "integrity": "sha512-SSJIhBr/teipXiXjmWOVWLnxjNGo65Oj/8wTEQz0nqwQeP75jWZ0n4sF24Zxoht1cuJoWopwj0J0exYwCJ0dCQ==", "license": "MIT", "dependencies": { "@vue/compiler-core": "3.5.16", @@ -5895,8 +4932,6 @@ }, "node_modules/@vue/compiler-sfc": { "version": "3.5.16", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.16.tgz", - "integrity": "sha512-rQR6VSFNpiinDy/DVUE0vHoIDUF++6p910cgcZoaAUm3POxgNOOdS/xgoll3rNdKYTYPnnbARDCZOyZ+QSe6Pw==", "license": "MIT", "dependencies": { "@babel/parser": "^7.27.2", @@ -5912,8 +4947,6 @@ }, "node_modules/@vue/compiler-ssr": { "version": "3.5.16", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.16.tgz", - "integrity": "sha512-d2V7kfxbdsjrDSGlJE7my1ZzCXViEcqN6w14DOsDrUCHEA6vbnVCpRFfrc4ryCP/lCKzX2eS1YtnLE/BuC9f/A==", "license": "MIT", "dependencies": { "@vue/compiler-dom": "3.5.16", @@ -5922,8 +4955,6 @@ }, "node_modules/@vue/devtools-api": { "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.7.tgz", - "integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==", "license": "MIT", "dependencies": { "@vue/devtools-kit": "^7.7.7" @@ -5931,8 +4962,6 @@ }, "node_modules/@vue/devtools-kit": { "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz", - "integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==", "license": "MIT", "dependencies": { "@vue/devtools-shared": "^7.7.7", @@ -5946,8 +4975,6 @@ }, "node_modules/@vue/devtools-shared": { "version": "7.7.7", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz", - "integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==", "license": "MIT", "dependencies": { "rfdc": "^1.4.1" @@ -5955,8 +4982,6 @@ }, "node_modules/@vue/reactivity": { "version": "3.5.16", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.16.tgz", - "integrity": "sha512-FG5Q5ee/kxhIm1p2bykPpPwqiUBV3kFySsHEQha5BJvjXdZTUfmya7wP7zC39dFuZAcf/PD5S4Lni55vGLMhvA==", "license": "MIT", "dependencies": { "@vue/shared": "3.5.16" @@ -5964,8 +4989,6 @@ }, "node_modules/@vue/runtime-core": { "version": "3.5.16", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.16.tgz", - "integrity": "sha512-bw5Ykq6+JFHYxrQa7Tjr+VSzw7Dj4ldR/udyBZbq73fCdJmyy5MPIFR9IX/M5Qs+TtTjuyUTCnmK3lWWwpAcFQ==", "license": "MIT", "dependencies": { "@vue/reactivity": "3.5.16", @@ -5974,8 +4997,6 @@ }, "node_modules/@vue/runtime-dom": { "version": "3.5.16", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.16.tgz", - "integrity": "sha512-T1qqYJsG2xMGhImRUV9y/RseB9d0eCYZQ4CWca9ztCuiPj/XWNNN+lkNBuzVbia5z4/cgxdL28NoQCvC0Xcfww==", "license": "MIT", "dependencies": { "@vue/reactivity": "3.5.16", @@ -5986,8 +5007,6 @@ }, "node_modules/@vue/server-renderer": { "version": "3.5.16", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.16.tgz", - "integrity": "sha512-BrX0qLiv/WugguGsnQUJiYOE0Fe5mZTwi6b7X/ybGB0vfrPH9z0gD/Y6WOR1sGCgX4gc25L1RYS5eYQKDMoNIg==", "license": "MIT", "dependencies": { "@vue/compiler-ssr": "3.5.16", @@ -5999,14 +5018,10 @@ }, "node_modules/@vue/shared": { "version": "3.5.16", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.16.tgz", - "integrity": "sha512-c/0fWy3Jw6Z8L9FmTyYfkpM5zklnqqa9+a6dz3DvONRKW2NEbh46BP0FHuLFSWi2TnQEtp91Z6zOWNrU6QiyPg==", "license": "MIT" }, "node_modules/@vueuse/core": { "version": "12.8.2", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", - "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", "license": "MIT", "dependencies": { "@types/web-bluetooth": "^0.0.21", @@ -6020,8 +5035,6 @@ }, "node_modules/@vueuse/metadata": { "version": "12.8.2", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", - "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" @@ -6029,8 +5042,6 @@ }, "node_modules/@vueuse/shared": { "version": "12.8.2", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", - "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", "license": "MIT", "dependencies": { "vue": "^3.5.13" @@ -6041,8 +5052,6 @@ }, "node_modules/@xmldom/xmldom": { "version": "0.9.8", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.8.tgz", - "integrity": "sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==", "license": "MIT", "engines": { "node": ">=14.6" @@ -6060,8 +5069,6 @@ }, "node_modules/accepts": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", "dependencies": { "mime-types": "^3.0.0", @@ -6073,8 +5080,6 @@ }, "node_modules/accepts/node_modules/mime-db": { "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -6082,8 +5087,6 @@ }, "node_modules/accepts/node_modules/mime-types": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -6093,13 +5096,11 @@ } }, "node_modules/ace-builds": { - "version": "1.37.5", + "version": "1.43.2", "license": "BSD-3-Clause" }, "node_modules/acorn": { "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -6139,9 +5140,8 @@ }, "node_modules/aggregate-error": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "dev": true, + "license": "MIT", "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -6166,8 +5166,6 @@ }, "node_modules/algoliasearch": { "version": "5.35.0", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.35.0.tgz", - "integrity": "sha512-Y+moNhsqgLmvJdgTsO4GZNgsaDWv8AOGAaPeIeHKlDn/XunoAqYbA+XNpBd1dW8GOXAUDyxC9Rxc7AV4kpFcIg==", "license": "MIT", "dependencies": { "@algolia/abtesting": "1.1.0", @@ -6191,9 +5189,8 @@ }, "node_modules/ansi-colors": { "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -6259,8 +5256,6 @@ }, "node_modules/arch": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", - "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", "dev": true, "funding": [ { @@ -6275,7 +5270,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/arg": { "version": "4.1.3", @@ -6419,27 +5415,24 @@ }, "node_modules/asn1": { "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "dev": true, + "license": "MIT", "dependencies": { "safer-buffer": "~2.1.0" } }, "node_modules/assert-plus": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8" } }, "node_modules/astral-regex": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -6463,9 +5456,8 @@ }, "node_modules/at-least-node": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true, + "license": "ISC", "engines": { "node": ">= 4.0.0" } @@ -6521,8 +5513,6 @@ }, "node_modules/aws-cdk": { "version": "2.1006.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1006.0.tgz", - "integrity": "sha512-6qYnCt4mBN+3i/5F+FC2yMETkDHY/IL7gt3EuqKVPcaAO4jU7oXfVSlR60CYRkZWL4fnAurUV14RkJuJyVG/IA==", "license": "Apache-2.0", "bin": { "cdk": "bin/cdk" @@ -6536,8 +5526,6 @@ }, "node_modules/aws-cdk-lib": { "version": "2.189.1", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.189.1.tgz", - "integrity": "sha512-9JU0yUr2iRTJ1oCPrHyx7hOtBDWyUfyOcdb6arlumJnMcQr2cyAMASY8HuAXHc8Y10ipVp8dRTW+J4/132IIYA==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -6927,27 +5915,25 @@ }, "node_modules/aws-sign2": { "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "*" } }, "node_modules/aws4": { "version": "1.13.2", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", - "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/axios": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", - "integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -7095,9 +6081,8 @@ }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "tweetnacl": "^0.14.3" } @@ -7114,8 +6099,6 @@ }, "node_modules/birpc": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.5.0.tgz", - "integrity": "sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" @@ -7123,20 +6106,16 @@ }, "node_modules/blob-util": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", - "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/bluebird": { "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/body-parser": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", @@ -7159,8 +6138,6 @@ }, "node_modules/brace-expansion": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -7237,9 +6214,8 @@ }, "node_modules/buffer-crc32": { "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -7250,8 +6226,6 @@ }, "node_modules/bundle-require": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", - "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", "dev": true, "license": "MIT", "dependencies": { @@ -7266,8 +6240,6 @@ }, "node_modules/bytes": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -7275,9 +6247,8 @@ }, "node_modules/cachedir": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", - "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -7300,8 +6271,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7313,8 +6282,6 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -7361,8 +6328,6 @@ }, "node_modules/caniuse-lite": { "version": "1.0.30001731", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", - "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", "dev": true, "funding": [ { @@ -7382,9 +6347,8 @@ }, "node_modules/caseless": { "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/ccount": { "version": "2.0.1", @@ -7396,8 +6360,7 @@ }, "node_modules/cdk-ecr-deployment": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cdk-ecr-deployment/-/cdk-ecr-deployment-4.0.1.tgz", - "integrity": "sha512-bv8JWQA+5KX0x+LTcwIaypEIDUQwgGa+H6hZJOwcPFikTOzYjvv3s4de8ivM2YIIJD0Jr/xxk+Z6UdKyUwQBYA==", + "license": "Apache-2.0", "peerDependencies": { "aws-cdk-lib": "^2.80.0", "constructs": "^10.0.5" @@ -7467,17 +6430,14 @@ }, "node_modules/check-more-types": { "version": "2.24.0", - "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", - "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } }, "node_modules/chevrotain": { "version": "11.0.3", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", - "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", "license": "Apache-2.0", "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", @@ -7490,8 +6450,6 @@ }, "node_modules/chevrotain-allstar": { "version": "0.3.1", - "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", - "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", "license": "MIT", "dependencies": { "lodash-es": "^4.17.21" @@ -7553,9 +6511,8 @@ }, "node_modules/clean-stack": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -7576,9 +6533,8 @@ }, "node_modules/cli-table3": { "version": "0.6.5", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", - "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", "dev": true, + "license": "MIT", "dependencies": { "string-width": "^4.2.0" }, @@ -7591,24 +6547,21 @@ }, "node_modules/cli-table3/node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cli-table3/node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/cli-table3/node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -7752,9 +6705,8 @@ }, "node_modules/common-tags": { "version": "1.8.2", - "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -7766,8 +6718,6 @@ }, "node_modules/confbox": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", "license": "MIT" }, "node_modules/console-table-printer": { @@ -7783,8 +6733,6 @@ }, "node_modules/content-disposition": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -7795,8 +6743,6 @@ }, "node_modules/content-type": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -7809,8 +6755,6 @@ }, "node_modules/cookie": { "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -7818,8 +6762,6 @@ }, "node_modules/cookie-signature": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", "engines": { "node": ">=6.6.0" @@ -7827,8 +6769,6 @@ }, "node_modules/copy-anything": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", - "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", "license": "MIT", "dependencies": { "is-what": "^4.1.8" @@ -7842,13 +6782,10 @@ }, "node_modules/core-util-is": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + "license": "MIT" }, "node_modules/cors": { "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "license": "MIT", "dependencies": { "object-assign": "^4", @@ -7860,8 +6797,6 @@ }, "node_modules/cose-base": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", - "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", "license": "MIT", "dependencies": { "layout-base": "^1.0.0" @@ -7884,8 +6819,6 @@ }, "node_modules/cosmiconfig/node_modules/yaml": { "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "dev": true, "license": "ISC", "engines": { @@ -7962,10 +6895,9 @@ }, "node_modules/cypress": { "version": "14.3.2", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.3.2.tgz", - "integrity": "sha512-n+yGD2ZFFKgy7I3YtVpZ7BcFYrrDMcKj713eOZdtxPttpBjCyw/R8dLlFSsJPouneGN7A/HOSRyPJ5+3/gKDoA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "@cypress/request": "^3.0.8", "@cypress/xvfb": "^1.2.4", @@ -8020,8 +6952,6 @@ }, "node_modules/cypress/node_modules/buffer": { "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, "funding": [ { @@ -8037,6 +6967,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -8044,8 +6975,6 @@ }, "node_modules/cypress/node_modules/ci-info": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.2.0.tgz", - "integrity": "sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==", "dev": true, "funding": [ { @@ -8053,15 +6982,15 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/cypress/node_modules/cli-cursor": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "dev": true, + "license": "MIT", "dependencies": { "restore-cursor": "^3.1.0" }, @@ -8071,9 +7000,8 @@ }, "node_modules/cypress/node_modules/cli-truncate": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", "dev": true, + "license": "MIT", "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" @@ -8087,24 +7015,21 @@ }, "node_modules/cypress/node_modules/commander": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/cypress/node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cypress/node_modules/execa": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", - "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.0", "get-stream": "^5.0.0", @@ -8125,9 +7050,8 @@ }, "node_modules/cypress/node_modules/get-stream": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, + "license": "MIT", "dependencies": { "pump": "^3.0.0" }, @@ -8140,27 +7064,24 @@ }, "node_modules/cypress/node_modules/human-signals": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=8.12.0" } }, "node_modules/cypress/node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/cypress/node_modules/listr2": { "version": "3.14.0", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", - "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", "dev": true, + "license": "MIT", "dependencies": { "cli-truncate": "^2.1.0", "colorette": "^2.0.16", @@ -8185,9 +7106,8 @@ }, "node_modules/cypress/node_modules/log-update": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", - "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", "dev": true, + "license": "MIT", "dependencies": { "ansi-escapes": "^4.3.0", "cli-cursor": "^3.1.0", @@ -8203,9 +7123,8 @@ }, "node_modules/cypress/node_modules/log-update/node_modules/slice-ansi": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", @@ -8220,9 +7139,8 @@ }, "node_modules/cypress/node_modules/log-update/node_modules/wrap-ansi": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -8234,15 +7152,13 @@ }, "node_modules/cypress/node_modules/proxy-from-env": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", - "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cypress/node_modules/restore-cursor": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, + "license": "MIT", "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" @@ -8253,9 +7169,8 @@ }, "node_modules/cypress/node_modules/slice-ansi": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", @@ -8267,9 +7182,8 @@ }, "node_modules/cypress/node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -8281,9 +7195,8 @@ }, "node_modules/cypress/node_modules/supports-color": { "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -8296,9 +7209,8 @@ }, "node_modules/cypress/node_modules/wrap-ansi": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -8313,8 +7225,6 @@ }, "node_modules/cytoscape": { "version": "3.33.1", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", - "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", "engines": { "node": ">=0.10" @@ -8322,8 +7232,6 @@ }, "node_modules/cytoscape-cose-bilkent": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", - "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", "license": "MIT", "dependencies": { "cose-base": "^1.0.0" @@ -8334,8 +7242,6 @@ }, "node_modules/cytoscape-fcose": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", - "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", "license": "MIT", "dependencies": { "cose-base": "^2.2.0" @@ -8346,8 +7252,6 @@ }, "node_modules/cytoscape-fcose/node_modules/cose-base": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", - "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", "license": "MIT", "dependencies": { "layout-base": "^2.0.0" @@ -8355,14 +7259,10 @@ }, "node_modules/cytoscape-fcose/node_modules/layout-base": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", - "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", "license": "MIT" }, "node_modules/d3": { "version": "7.9.0", - "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", - "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", "license": "ISC", "dependencies": { "d3-array": "3", @@ -8402,8 +7302,6 @@ }, "node_modules/d3-array": { "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", "license": "ISC", "dependencies": { "internmap": "1 - 2" @@ -8414,8 +7312,6 @@ }, "node_modules/d3-axis": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", - "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", "license": "ISC", "engines": { "node": ">=12" @@ -8423,8 +7319,6 @@ }, "node_modules/d3-brush": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", - "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", @@ -8439,8 +7333,6 @@ }, "node_modules/d3-chord": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", - "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", "license": "ISC", "dependencies": { "d3-path": "1 - 3" @@ -8451,8 +7343,6 @@ }, "node_modules/d3-color": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", "license": "ISC", "engines": { "node": ">=12" @@ -8460,8 +7350,6 @@ }, "node_modules/d3-contour": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", - "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", "license": "ISC", "dependencies": { "d3-array": "^3.2.0" @@ -8472,8 +7360,6 @@ }, "node_modules/d3-delaunay": { "version": "6.0.4", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", "license": "ISC", "dependencies": { "delaunator": "5" @@ -8484,8 +7370,6 @@ }, "node_modules/d3-dispatch": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", "license": "ISC", "engines": { "node": ">=12" @@ -8493,8 +7377,6 @@ }, "node_modules/d3-drag": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", @@ -8506,8 +7388,6 @@ }, "node_modules/d3-dsv": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", - "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", "license": "ISC", "dependencies": { "commander": "7", @@ -8531,8 +7411,6 @@ }, "node_modules/d3-dsv/node_modules/commander": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "license": "MIT", "engines": { "node": ">= 10" @@ -8540,8 +7418,6 @@ }, "node_modules/d3-ease": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", "license": "BSD-3-Clause", "engines": { "node": ">=12" @@ -8549,8 +7425,6 @@ }, "node_modules/d3-fetch": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", - "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", "license": "ISC", "dependencies": { "d3-dsv": "1 - 3" @@ -8561,8 +7435,6 @@ }, "node_modules/d3-force": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", - "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", @@ -8575,8 +7447,6 @@ }, "node_modules/d3-format": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", "license": "ISC", "engines": { "node": ">=12" @@ -8584,8 +7454,6 @@ }, "node_modules/d3-geo": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", - "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", "license": "ISC", "dependencies": { "d3-array": "2.5.0 - 3" @@ -8596,8 +7464,6 @@ }, "node_modules/d3-hierarchy": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", - "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", "license": "ISC", "engines": { "node": ">=12" @@ -8605,8 +7471,6 @@ }, "node_modules/d3-interpolate": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", "license": "ISC", "dependencies": { "d3-color": "1 - 3" @@ -8621,8 +7485,6 @@ }, "node_modules/d3-polygon": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", - "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", "license": "ISC", "engines": { "node": ">=12" @@ -8630,8 +7492,6 @@ }, "node_modules/d3-quadtree": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", - "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", "license": "ISC", "engines": { "node": ">=12" @@ -8639,8 +7499,6 @@ }, "node_modules/d3-random": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", - "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", "license": "ISC", "engines": { "node": ">=12" @@ -8648,8 +7506,6 @@ }, "node_modules/d3-sankey": { "version": "0.12.3", - "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", - "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", "license": "BSD-3-Clause", "dependencies": { "d3-array": "1 - 2", @@ -8658,8 +7514,6 @@ }, "node_modules/d3-sankey/node_modules/d3-array": { "version": "2.12.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", - "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", "license": "BSD-3-Clause", "dependencies": { "internmap": "^1.0.0" @@ -8667,14 +7521,10 @@ }, "node_modules/d3-sankey/node_modules/internmap": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", - "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", "license": "ISC" }, "node_modules/d3-scale": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", "license": "ISC", "dependencies": { "d3-array": "2.10.0 - 3", @@ -8689,8 +7539,6 @@ }, "node_modules/d3-scale-chromatic": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", "license": "ISC", "dependencies": { "d3-color": "1 - 3", @@ -8702,8 +7550,6 @@ }, "node_modules/d3-selection": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", "engines": { "node": ">=12" @@ -8718,8 +7564,6 @@ }, "node_modules/d3-time": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", "license": "ISC", "dependencies": { "d3-array": "2 - 3" @@ -8730,8 +7574,6 @@ }, "node_modules/d3-time-format": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", "license": "ISC", "dependencies": { "d3-time": "1 - 3" @@ -8742,8 +7584,6 @@ }, "node_modules/d3-timer": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", "license": "ISC", "engines": { "node": ">=12" @@ -8751,8 +7591,6 @@ }, "node_modules/d3-transition": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", "license": "ISC", "dependencies": { "d3-color": "1 - 3", @@ -8770,8 +7608,6 @@ }, "node_modules/d3-zoom": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", @@ -8786,8 +7622,6 @@ }, "node_modules/d3/node_modules/d3-path": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", "license": "ISC", "engines": { "node": ">=12" @@ -8795,8 +7629,6 @@ }, "node_modules/d3/node_modules/d3-shape": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", "license": "ISC", "dependencies": { "d3-path": "^3.1.0" @@ -8807,8 +7639,6 @@ }, "node_modules/dagre-d3-es": { "version": "7.0.11", - "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz", - "integrity": "sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==", "license": "MIT", "dependencies": { "d3": "^7.9.0", @@ -8817,9 +7647,8 @@ }, "node_modules/dashdash": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "dev": true, + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0" }, @@ -8891,11 +7720,10 @@ }, "node_modules/dayjs": { "version": "1.11.13", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + "license": "MIT" }, "node_modules/debug": { - "version": "4.4.0", + "version": "4.4.3", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -8990,8 +7818,6 @@ }, "node_modules/delaunator": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", "license": "ISC", "dependencies": { "robust-predicates": "^3.0.2" @@ -9076,8 +7902,6 @@ }, "node_modules/depd": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -9134,6 +7958,10 @@ "node": ">=0.3.1" } }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "license": "Apache-2.0" + }, "node_modules/diff-sequences": { "version": "29.6.3", "dev": true, @@ -9178,8 +8006,6 @@ }, "node_modules/dompurify": { "version": "3.2.6", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", - "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -9203,9 +8029,8 @@ }, "node_modules/ecc-jsbn": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "dev": true, + "license": "MIT", "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -9217,8 +8042,6 @@ }, "node_modules/ee-first": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, "node_modules/ejs": { @@ -9258,14 +8081,10 @@ }, "node_modules/emoji-regex-xs": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", - "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", "license": "MIT" }, "node_modules/encodeurl": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -9273,18 +8092,16 @@ }, "node_modules/end-of-stream": { "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, + "license": "MIT", "dependencies": { "once": "^1.4.0" } }, "node_modules/enquirer": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", - "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" @@ -9449,8 +8266,6 @@ }, "node_modules/esbuild": { "version": "0.25.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -9497,8 +8312,6 @@ }, "node_modules/escape-html": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, "node_modules/escape-string-regexp": { @@ -9653,8 +8466,6 @@ }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -9776,8 +8587,6 @@ }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -9850,8 +8659,6 @@ }, "node_modules/esm": { "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", "license": "MIT", "engines": { "node": ">=6" @@ -9925,8 +8732,6 @@ }, "node_modules/estree-walker": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, "node_modules/esutils": { @@ -9939,8 +8744,6 @@ }, "node_modules/etag": { "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -9955,9 +8758,8 @@ }, "node_modules/eventemitter2": { "version": "6.4.7", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", - "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/eventemitter3": { "version": "5.0.1", @@ -9973,8 +8775,6 @@ }, "node_modules/eventsource": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", "license": "MIT", "dependencies": { "eventsource-parser": "^3.0.1" @@ -9985,8 +8785,6 @@ }, "node_modules/eventsource-parser": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.3.tgz", - "integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==", "license": "MIT", "engines": { "node": ">=20.0.0" @@ -10016,9 +8814,8 @@ }, "node_modules/executable": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", - "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", "dev": true, + "license": "MIT", "dependencies": { "pify": "^2.2.0" }, @@ -10061,8 +8858,6 @@ }, "node_modules/express": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", @@ -10103,8 +8898,6 @@ }, "node_modules/express-rate-limit": { "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", "license": "MIT", "engines": { "node": ">= 16" @@ -10118,8 +8911,6 @@ }, "node_modules/express/node_modules/mime-db": { "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -10127,8 +8918,6 @@ }, "node_modules/express/node_modules/mime-types": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -10139,8 +8928,6 @@ }, "node_modules/exsolve": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", - "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", "license": "MIT" }, "node_modules/extend": { @@ -10149,9 +8936,8 @@ }, "node_modules/extract-zip": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", @@ -10169,9 +8955,8 @@ }, "node_modules/extract-zip/node_modules/get-stream": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, + "license": "MIT", "dependencies": { "pump": "^3.0.0" }, @@ -10184,12 +8969,11 @@ }, "node_modules/extsprintf": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "dev": true, "engines": [ "node >=0.6.0" - ] + ], + "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", @@ -10266,8 +9050,6 @@ }, "node_modules/fault": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", - "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", "license": "MIT", "dependencies": { "format": "^0.2.0" @@ -10287,17 +9069,14 @@ }, "node_modules/fd-slicer": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dev": true, + "license": "MIT", "dependencies": { "pend": "~1.2.0" } }, "node_modules/fdir": { "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", "license": "MIT", "peerDependencies": { "picomatch": "^3 || ^4" @@ -10310,9 +9089,8 @@ }, "node_modules/figures": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "^1.0.5" }, @@ -10325,9 +9103,8 @@ }, "node_modules/figures/node_modules/escape-string-regexp": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.0" } @@ -10374,8 +9151,6 @@ }, "node_modules/finalhandler": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -10438,8 +9213,6 @@ }, "node_modules/focus-trap": { "version": "7.6.5", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.5.tgz", - "integrity": "sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg==", "license": "MIT", "dependencies": { "tabbable": "^6.2.0" @@ -10502,17 +9275,14 @@ }, "node_modules/forever-agent": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "*" } }, "node_modules/form-data": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -10531,8 +9301,6 @@ }, "node_modules/format": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", - "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", "engines": { "node": ">=0.4.x" } @@ -10550,8 +9318,6 @@ }, "node_modules/forwarded": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -10571,8 +9337,6 @@ }, "node_modules/fresh": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -10580,9 +9344,8 @@ }, "node_modules/fs-extra": { "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", "dev": true, + "license": "MIT", "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", @@ -10675,8 +9438,6 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -10756,18 +9517,16 @@ }, "node_modules/getos": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", - "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", "dev": true, + "license": "MIT", "dependencies": { "async": "^3.2.0" } }, "node_modules/getpass": { "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "dev": true, + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0" } @@ -10810,8 +9569,6 @@ }, "node_modules/glob/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -10832,9 +9589,8 @@ }, "node_modules/global-dirs": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", - "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", "dev": true, + "license": "MIT", "dependencies": { "ini": "2.0.0" }, @@ -10847,9 +9603,8 @@ }, "node_modules/global-dirs/node_modules/ini": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } @@ -10894,9 +9649,7 @@ } }, "node_modules/globals": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", - "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "version": "16.4.0", "dev": true, "license": "MIT", "engines": { @@ -10962,8 +9715,6 @@ }, "node_modules/hachure-fill": { "version": "0.5.2", - "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", - "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", "license": "MIT" }, "node_modules/has-bigints": { @@ -11043,8 +9794,6 @@ }, "node_modules/hast-util-is-element": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", - "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" @@ -11056,8 +9805,6 @@ }, "node_modules/hast-util-parse-selector": { "version": "2.2.5", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", - "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", "license": "MIT", "funding": { "type": "opencollective", @@ -11066,8 +9813,6 @@ }, "node_modules/hast-util-to-html": { "version": "9.0.5", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", - "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -11089,8 +9834,6 @@ }, "node_modules/hast-util-to-html/node_modules/property-information": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", "license": "MIT", "funding": { "type": "github", @@ -11124,8 +9867,6 @@ }, "node_modules/hast-util-to-text": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", - "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -11151,8 +9892,6 @@ }, "node_modules/hastscript": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", - "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", "license": "MIT", "dependencies": { "@types/hast": "^2.0.0", @@ -11168,8 +9907,6 @@ }, "node_modules/hastscript/node_modules/@types/hast": { "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", - "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", "license": "MIT", "dependencies": { "@types/unist": "^2" @@ -11177,14 +9914,10 @@ }, "node_modules/hastscript/node_modules/@types/unist": { "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, "node_modules/hastscript/node_modules/comma-separated-tokens": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", - "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", "license": "MIT", "funding": { "type": "github", @@ -11193,8 +9926,6 @@ }, "node_modules/hastscript/node_modules/property-information": { "version": "5.6.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", - "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", "license": "MIT", "dependencies": { "xtend": "^4.0.0" @@ -11206,8 +9937,6 @@ }, "node_modules/hastscript/node_modules/space-separated-tokens": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", - "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", "license": "MIT", "funding": { "type": "github", @@ -11216,8 +9945,6 @@ }, "node_modules/highlight.js": { "version": "10.7.3", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", - "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", "license": "BSD-3-Clause", "engines": { "node": "*" @@ -11225,8 +9952,6 @@ }, "node_modules/highlightjs-vue": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", - "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", "license": "CC0-1.0" }, "node_modules/hoist-non-react-statics": { @@ -11253,8 +9978,6 @@ }, "node_modules/hookable": { "version": "5.5.3", - "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", - "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", "license": "MIT" }, "node_modules/html-escaper": { @@ -11272,8 +9995,6 @@ }, "node_modules/html-void-elements": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", - "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", "license": "MIT", "funding": { "type": "github", @@ -11282,8 +10003,6 @@ }, "node_modules/http-errors": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "license": "MIT", "dependencies": { "depd": "2.0.0", @@ -11298,8 +10017,6 @@ }, "node_modules/http-errors/node_modules/statuses": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -11307,9 +10024,8 @@ }, "node_modules/http-signature": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", - "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", "dev": true, + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^2.0.2", @@ -11336,8 +10052,6 @@ }, "node_modules/husky": { "version": "9.1.7", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", - "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", "dev": true, "license": "MIT", "bin": { @@ -11352,8 +10066,6 @@ }, "node_modules/iconv-lite": { "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -11376,8 +10088,6 @@ }, "node_modules/immediate": { "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "license": "MIT" }, "node_modules/immer": { @@ -11439,9 +10149,8 @@ }, "node_modules/indent-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -11483,8 +10192,6 @@ }, "node_modules/internmap": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", "license": "ISC", "engines": { "node": ">=12" @@ -11502,8 +10209,6 @@ }, "node_modules/ipaddr.js": { "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "license": "MIT", "engines": { "node": ">= 0.10" @@ -11759,9 +10464,8 @@ }, "node_modules/is-installed-globally": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", - "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", "dev": true, + "license": "MIT", "dependencies": { "global-dirs": "^3.0.0", "is-path-inside": "^3.0.2" @@ -11826,8 +10530,6 @@ }, "node_modules/is-promise": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, "node_modules/is-regex": { @@ -11928,15 +10630,13 @@ }, "node_modules/is-typedarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-unicode-supported": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -11986,8 +10686,6 @@ }, "node_modules/is-what": { "version": "4.1.16", - "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", - "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", "license": "MIT", "engines": { "node": ">=12.13" @@ -12014,9 +10712,8 @@ }, "node_modules/isstream": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -12111,8 +10808,6 @@ }, "node_modules/jake/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -12755,9 +11450,8 @@ }, "node_modules/joi": { "version": "17.13.3", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", - "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", @@ -12789,9 +11483,8 @@ }, "node_modules/jsbn": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/jsesc": { "version": "3.1.0", @@ -12816,9 +11509,8 @@ }, "node_modules/json-schema": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "dev": true + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -12831,9 +11523,8 @@ }, "node_modules/json-stringify-safe": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/json5": { "version": "2.2.3", @@ -12848,9 +11539,8 @@ }, "node_modules/jsonfile": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, + "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -12867,12 +11557,11 @@ }, "node_modules/jsprim": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", - "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", "dev": true, "engines": [ "node >=0.6.0" ], + "license": "MIT", "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -12882,8 +11571,6 @@ }, "node_modules/jszip": { "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "license": "(MIT OR GPL-3.0-or-later)", "dependencies": { "lie": "~3.3.0", @@ -12899,8 +11586,6 @@ }, "node_modules/katex": { "version": "0.16.22", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz", - "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -12915,8 +11600,6 @@ }, "node_modules/katex/node_modules/commander": { "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "license": "MIT", "engines": { "node": ">= 12" @@ -12931,9 +11614,7 @@ } }, "node_modules/khroma": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", - "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + "version": "2.1.0" }, "node_modules/kleur": { "version": "3.0.3", @@ -12945,8 +11626,6 @@ }, "node_modules/kolorist": { "version": "1.8.0", - "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", - "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", "license": "MIT" }, "node_modules/langchain": { @@ -13052,8 +11731,6 @@ }, "node_modules/langium": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz", - "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", "license": "MIT", "dependencies": { "chevrotain": "~11.0.3", @@ -13104,15 +11781,12 @@ }, "node_modules/layout-base": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", - "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", "license": "MIT" }, "node_modules/lazy-ass": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", - "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", "dev": true, + "license": "MIT", "engines": { "node": "> 0.8" } @@ -13139,8 +11813,6 @@ }, "node_modules/lie": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "license": "MIT", "dependencies": { "immediate": "~3.0.5" @@ -13170,9 +11842,8 @@ }, "node_modules/lint-staged": { "version": "15.5.1", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.1.tgz", - "integrity": "sha512-6m7u8mue4Xn6wK6gZvSCQwBvMBR36xfY24nF5bMTf2MHDYG6S3yhJuOgdYVw99hsjyDt2d4z168b3naI8+NWtQ==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^5.4.1", "commander": "^13.1.0", @@ -13356,8 +12027,6 @@ }, "node_modules/load-tsconfig": { "version": "0.2.5", - "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", - "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", "dev": true, "license": "MIT", "engines": { @@ -13366,8 +12035,6 @@ }, "node_modules/local-pkg": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", - "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", "license": "MIT", "dependencies": { "mlly": "^1.7.4", @@ -13401,8 +12068,14 @@ }, "node_modules/lodash-es": { "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", "license": "MIT" }, "node_modules/lodash.isplainobject": { @@ -13422,15 +12095,13 @@ }, "node_modules/lodash.once": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/log-symbols": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" @@ -13559,8 +12230,6 @@ }, "node_modules/lowlight": { "version": "1.20.0", - "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", - "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", "license": "MIT", "dependencies": { "fault": "^1.0.0", @@ -13622,8 +12291,6 @@ }, "node_modules/mark.js": { "version": "8.11.1", - "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", - "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", "license": "MIT" }, "node_modules/markdown-it": { @@ -13643,15 +12310,13 @@ } }, "node_modules/marked": { - "version": "16.2.1", - "resolved": "https://registry.npmjs.org/marked/-/marked-16.2.1.tgz", - "integrity": "sha512-r3UrXED9lMlHF97jJByry90cwrZBBvZmjG1L68oYfuPMW+uDTnuMbyJDymCWwbTE+f+3LhpNDKfpR3a3saFyjA==", + "version": "15.0.12", "license": "MIT", "bin": { "marked": "bin/marked.js" }, "engines": { - "node": ">= 20" + "node": ">= 18" } }, "node_modules/math-intrinsics": { @@ -13663,8 +12328,6 @@ }, "node_modules/mathjax-full": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.2.tgz", - "integrity": "sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==", "license": "Apache-2.0", "dependencies": { "esm": "^3.2.25", @@ -13721,8 +12384,6 @@ }, "node_modules/mdast-util-math": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", - "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -13872,8 +12533,6 @@ }, "node_modules/media-typer": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -13881,8 +12540,6 @@ }, "node_modules/merge-descriptors": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", "engines": { "node": ">=18" @@ -13904,13 +12561,11 @@ } }, "node_modules/mermaid": { - "version": "11.10.1", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.10.1.tgz", - "integrity": "sha512-0PdeADVWURz7VMAX0+MiMcgfxFKY4aweSGsjgFihe3XlMKNqmai/cugMrqTd3WNHM93V+K+AZL6Wu6tB5HmxRw==", + "version": "11.11.0", "license": "MIT", "dependencies": { "@braintree/sanitize-url": "^7.0.4", - "@iconify/utils": "^2.1.33", + "@iconify/utils": "^3.0.1", "@mermaid-js/parser": "^0.6.2", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", @@ -13924,7 +12579,7 @@ "katex": "^0.16.22", "khroma": "^2.1.0", "lodash-es": "^4.17.21", - "marked": "^16.0.0", + "marked": "^15.0.7", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", @@ -13933,8 +12588,6 @@ }, "node_modules/mermaid/node_modules/uuid": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -13946,14 +12599,10 @@ }, "node_modules/mhchemparser": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.2.1.tgz", - "integrity": "sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==", "license": "Apache-2.0" }, "node_modules/micromark": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", - "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", "funding": [ { "type": "GitHub Sponsors", @@ -13964,6 +12613,7 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", @@ -13986,8 +12636,6 @@ }, "node_modules/micromark-core-commonmark": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.1.tgz", - "integrity": "sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==", "funding": [ { "type": "GitHub Sponsors", @@ -13998,6 +12646,7 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", @@ -14019,8 +12668,6 @@ }, "node_modules/micromark-extension-math": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", - "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", "license": "MIT", "dependencies": { "@types/katex": "^0.16.0", @@ -14459,26 +13106,18 @@ }, "node_modules/minisearch": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.1.2.tgz", - "integrity": "sha512-R1Pd9eF+MD5JYDDSPAp/q1ougKglm14uEkPMvQ/05RGmx6G9wvmLTrTI/Q5iPNJLYqNdsDQ7qTGIcNWR+FrHmA==", "license": "MIT" }, "node_modules/mitt": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "license": "MIT" }, "node_modules/mj-context-menu": { "version": "0.6.1", - "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz", - "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==", "license": "Apache-2.0" }, "node_modules/mlly": { "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", "license": "MIT", "dependencies": { "acorn": "^8.15.0", @@ -14489,14 +13128,10 @@ }, "node_modules/mlly/node_modules/confbox": { "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", "license": "MIT" }, "node_modules/mlly/node_modules/pkg-types": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", "license": "MIT", "dependencies": { "confbox": "^0.1.8", @@ -14538,8 +13173,6 @@ }, "node_modules/multimatch/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -14597,8 +13230,6 @@ }, "node_modules/negotiator": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -14787,8 +13418,6 @@ }, "node_modules/on-finished": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -14820,8 +13449,6 @@ }, "node_modules/oniguruma-to-es": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", - "integrity": "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==", "license": "MIT", "dependencies": { "emoji-regex-xs": "^1.0.0", @@ -14890,9 +13517,8 @@ }, "node_modules/ospath": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", - "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/own-keys": { "version": "1.0.1", @@ -14947,9 +13573,8 @@ }, "node_modules/p-map": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "dev": true, + "license": "MIT", "dependencies": { "aggregate-error": "^3.0.0" }, @@ -15013,14 +13638,10 @@ }, "node_modules/package-manager-detector": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz", - "integrity": "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==", "license": "MIT" }, "node_modules/pako": { "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, "node_modules/parent-module": { @@ -15082,8 +13703,6 @@ }, "node_modules/parseurl": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -15091,8 +13710,6 @@ }, "node_modules/path-data-parser": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", - "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", "license": "MIT" }, "node_modules/path-exists": { @@ -15142,8 +13759,6 @@ }, "node_modules/path-to-regexp": { "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", "license": "MIT", "engines": { "node": ">=16" @@ -15159,27 +13774,21 @@ }, "node_modules/pathe": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, "node_modules/pend": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/perfect-debounce": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", - "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", "license": "MIT" }, "node_modules/performance-now": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", @@ -15222,8 +13831,6 @@ }, "node_modules/pkce-challenge": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", "license": "MIT", "engines": { "node": ">=16.20.0" @@ -15290,8 +13897,6 @@ }, "node_modules/pkg-types": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", "license": "MIT", "dependencies": { "confbox": "^0.2.2", @@ -15309,14 +13914,10 @@ }, "node_modules/points-on-curve": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", - "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", "license": "MIT" }, "node_modules/points-on-path": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", - "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", "license": "MIT", "dependencies": { "path-data-parser": "0.1.0", @@ -15332,8 +13933,6 @@ }, "node_modules/postcss": { "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "funding": [ { "type": "opencollective", @@ -15463,8 +14062,6 @@ }, "node_modules/preact": { "version": "10.27.1", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.1.tgz", - "integrity": "sha512-V79raXEWch/rbqoNc7nT9E4ep7lu+mI3+sBmfRD4i1M73R3WLYcCtdI0ibxGVf4eQL8ZIz2nFacqEC+rmnOORQ==", "license": "MIT", "funding": { "type": "opencollective", @@ -15506,9 +14103,8 @@ }, "node_modules/pretty-bytes": { "version": "5.6.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", - "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" }, @@ -15542,8 +14138,6 @@ }, "node_modules/prismjs": { "version": "1.30.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", - "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", "license": "MIT", "engines": { "node": ">=6" @@ -15551,17 +14145,14 @@ }, "node_modules/process": { "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6.0" } }, "node_modules/process-nextick-args": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, "node_modules/prompts": { @@ -15599,8 +14190,6 @@ }, "node_modules/proxy-addr": { "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -15616,9 +14205,8 @@ }, "node_modules/pump": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", "dev": true, + "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -15656,8 +14244,7 @@ }, "node_modules/qs": { "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" }, @@ -15670,8 +14257,6 @@ }, "node_modules/quansync": { "version": "0.2.11", - "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", - "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", "funding": [ { "type": "individual", @@ -15710,8 +14295,6 @@ }, "node_modules/range-parser": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -15719,8 +14302,6 @@ }, "node_modules/raw-body": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -15742,6 +14323,21 @@ "node": ">=0.10.0" } }, + "node_modules/react-ace": { + "version": "14.0.1", + "license": "MIT", + "dependencies": { + "ace-builds": "^1.36.3", + "diff-match-patch": "^1.0.5", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "18.3.1", "license": "MIT", @@ -15879,8 +14475,6 @@ }, "node_modules/react-syntax-highlighter": { "version": "15.6.6", - "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz", - "integrity": "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", @@ -15932,8 +14526,6 @@ }, "node_modules/readable-stream": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -15947,8 +14539,6 @@ }, "node_modules/readable-stream/node_modules/safe-buffer": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, "node_modules/readdirp": { @@ -16034,8 +14624,6 @@ }, "node_modules/refractor": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", - "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", "license": "MIT", "dependencies": { "hastscript": "^6.0.0", @@ -16049,8 +14637,6 @@ }, "node_modules/refractor/node_modules/character-entities": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", - "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", "license": "MIT", "funding": { "type": "github", @@ -16059,8 +14645,6 @@ }, "node_modules/refractor/node_modules/character-entities-legacy": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", - "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", "license": "MIT", "funding": { "type": "github", @@ -16069,8 +14653,6 @@ }, "node_modules/refractor/node_modules/character-reference-invalid": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", - "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", "license": "MIT", "funding": { "type": "github", @@ -16079,8 +14661,6 @@ }, "node_modules/refractor/node_modules/is-alphabetical": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", "license": "MIT", "funding": { "type": "github", @@ -16089,8 +14669,6 @@ }, "node_modules/refractor/node_modules/is-alphanumerical": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", "license": "MIT", "dependencies": { "is-alphabetical": "^1.0.0", @@ -16103,8 +14681,6 @@ }, "node_modules/refractor/node_modules/is-decimal": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", "license": "MIT", "funding": { "type": "github", @@ -16113,8 +14689,6 @@ }, "node_modules/refractor/node_modules/is-hexadecimal": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", "license": "MIT", "funding": { "type": "github", @@ -16123,8 +14697,6 @@ }, "node_modules/refractor/node_modules/parse-entities": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", - "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", "license": "MIT", "dependencies": { "character-entities": "^1.0.0", @@ -16141,8 +14713,6 @@ }, "node_modules/refractor/node_modules/prismjs": { "version": "1.27.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", - "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", "license": "MIT", "engines": { "node": ">=6" @@ -16154,8 +14724,6 @@ }, "node_modules/regex": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz", - "integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==", "license": "MIT", "dependencies": { "regex-utilities": "^2.3.0" @@ -16163,8 +14731,6 @@ }, "node_modules/regex-recursion": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", - "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", "license": "MIT", "dependencies": { "regex-utilities": "^2.3.0" @@ -16172,8 +14738,6 @@ }, "node_modules/regex-utilities": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", - "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", "license": "MIT" }, "node_modules/regexp.prototype.flags": { @@ -16197,8 +14761,6 @@ }, "node_modules/rehype-mathjax": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/rehype-mathjax/-/rehype-mathjax-7.1.0.tgz", - "integrity": "sha512-mJHNpoqCC5UZ24OKx0wNjlzV18qeJz/Q/LtEjxXzt8vqrZ1Z3GxQnVrHcF5/PogcXUK8cWwJ4U/LWOQWEiABHw==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -16217,8 +14779,6 @@ }, "node_modules/rehype-mathjax/node_modules/hast-util-parse-selector": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", - "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0" @@ -16230,8 +14790,6 @@ }, "node_modules/rehype-mathjax/node_modules/hastscript": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", - "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -16247,8 +14805,6 @@ }, "node_modules/rehype-mathjax/node_modules/property-information": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", "license": "MIT", "funding": { "type": "github", @@ -16270,8 +14826,6 @@ }, "node_modules/remark-math": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", - "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -16315,9 +14869,8 @@ }, "node_modules/request-progress": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", - "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", "dev": true, + "license": "MIT", "dependencies": { "throttleit": "^1.0.0" } @@ -16479,14 +15032,10 @@ }, "node_modules/robust-predicates": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", "license": "Unlicense" }, "node_modules/rollup": { "version": "4.34.9", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.9.tgz", - "integrity": "sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==", "license": "MIT", "dependencies": { "@types/estree": "1.0.6" @@ -16523,8 +15072,6 @@ }, "node_modules/roughjs": { "version": "4.6.6", - "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", - "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", "license": "MIT", "dependencies": { "hachure-fill": "^0.5.2", @@ -16535,8 +15082,6 @@ }, "node_modules/router": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -16572,15 +15117,12 @@ }, "node_modules/rw": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", "license": "BSD-3-Clause" }, "node_modules/rxjs": { "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" } @@ -16610,8 +15152,6 @@ }, "node_modules/safe-buffer": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "funding": [ { "type": "github", @@ -16625,7 +15165,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/safe-push-apply": { "version": "1.0.0", @@ -16664,8 +15205,7 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "license": "MIT" }, "node_modules/sax": { "version": "1.2.1", @@ -16680,8 +15220,6 @@ }, "node_modules/search-insights": { "version": "2.17.3", - "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", - "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", "license": "MIT", "peer": true }, @@ -16702,8 +15240,6 @@ }, "node_modules/send": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "license": "MIT", "dependencies": { "debug": "^4.3.5", @@ -16724,8 +15260,6 @@ }, "node_modules/send/node_modules/mime-db": { "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -16733,8 +15267,6 @@ }, "node_modules/send/node_modules/mime-types": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -16745,8 +15277,6 @@ }, "node_modules/serve-static": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", @@ -16802,14 +15332,10 @@ }, "node_modules/setimmediate": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "license": "MIT" }, "node_modules/setprototypeof": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, "node_modules/shebang-command": { @@ -16831,8 +15357,6 @@ }, "node_modules/shiki": { "version": "2.5.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz", - "integrity": "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==", "license": "MIT", "dependencies": { "@shikijs/core": "2.5.0", @@ -16989,8 +15513,6 @@ }, "node_modules/speakingurl": { "version": "14.0.1", - "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", - "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -16998,8 +15520,6 @@ }, "node_modules/speech-rule-engine": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-4.1.2.tgz", - "integrity": "sha512-S6ji+flMEga+1QU79NDbwZ8Ivf0S/MpupQQiIC0rTpU/ZTKgcajijJJb1OcByBQDjrXCN1/DJtGz4ZJeBMPGJw==", "license": "Apache-2.0", "dependencies": { "@xmldom/xmldom": "0.9.8", @@ -17017,9 +15537,8 @@ }, "node_modules/sshpk": { "version": "1.18.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", - "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", "dev": true, + "license": "MIT", "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -17061,8 +15580,6 @@ }, "node_modules/statuses": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -17070,14 +15587,10 @@ }, "node_modules/strict-url-sanitise": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/strict-url-sanitise/-/strict-url-sanitise-0.0.1.tgz", - "integrity": "sha512-nuFtF539K8jZg3FjaWH/L8eocCR6gegz5RDOsaWxfdbF5Jqr2VXWxZayjTwUzsWJDC91k2EbnJXp6FuWW+Z4hg==", "license": "MIT" }, "node_modules/string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -17085,8 +15598,6 @@ }, "node_modules/string_decoder/node_modules/safe-buffer": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, "node_modules/string-argv": { @@ -17300,8 +15811,6 @@ }, "node_modules/stylis": { "version": "4.3.6", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", - "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", "license": "MIT" }, "node_modules/sucrase": { @@ -17364,8 +15873,6 @@ }, "node_modules/superjson": { "version": "2.2.2", - "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", - "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", "license": "MIT", "dependencies": { "copy-anything": "^3.0.2" @@ -17411,8 +15918,6 @@ }, "node_modules/tabbable": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", "license": "MIT" }, "node_modules/tailwindcss": { @@ -17456,8 +15961,6 @@ }, "node_modules/terser": { "version": "5.39.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", - "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", "license": "BSD-2-Clause", "optional": true, "peer": true, @@ -17476,8 +15979,6 @@ }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "license": "MIT", "optional": true, "peer": true @@ -17497,8 +15998,6 @@ }, "node_modules/test-exclude/node_modules/brace-expansion": { "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -17541,29 +16040,23 @@ }, "node_modules/throttleit": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", - "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/through": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/tinyexec": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", - "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", "license": "MIT" }, "node_modules/tinyglobby": { "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", "license": "MIT", "dependencies": { "fdir": "^6.4.4", @@ -17578,9 +16071,8 @@ }, "node_modules/tldts": { "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", - "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", "dev": true, + "license": "MIT", "dependencies": { "tldts-core": "^6.1.86" }, @@ -17590,14 +16082,11 @@ }, "node_modules/tldts-core": { "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", - "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/tmp": { "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", "dev": true, "license": "MIT", "engines": { @@ -17621,8 +16110,6 @@ }, "node_modules/toidentifier": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "license": "MIT", "engines": { "node": ">=0.6" @@ -17630,9 +16117,8 @@ }, "node_modules/tough-cookie": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", - "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "tldts": "^6.1.32" }, @@ -17646,9 +16132,8 @@ }, "node_modules/tree-kill": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, + "license": "MIT", "bin": { "tree-kill": "cli.js" } @@ -17682,8 +16167,6 @@ }, "node_modules/ts-dedent": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", - "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", "license": "MIT", "engines": { "node": ">=6.10" @@ -17836,9 +16319,8 @@ }, "node_modules/tunnel-agent": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, + "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" }, @@ -17848,9 +16330,8 @@ }, "node_modules/tweetnacl": { "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "dev": true + "dev": true, + "license": "Unlicense" }, "node_modules/type-check": { "version": "0.4.0", @@ -17884,8 +16365,6 @@ }, "node_modules/type-is": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "dependencies": { "content-type": "^1.0.5", @@ -17898,8 +16377,6 @@ }, "node_modules/type-is/node_modules/mime-db": { "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -17907,8 +16384,6 @@ }, "node_modules/type-is/node_modules/mime-types": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -18005,8 +16480,6 @@ }, "node_modules/ufo": { "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", "license": "MIT" }, "node_modules/unbox-primitive": { @@ -18028,8 +16501,7 @@ }, "node_modules/undici-types": { "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + "license": "MIT" }, "node_modules/unified": { "version": "11.0.5", @@ -18050,8 +16522,6 @@ }, "node_modules/unist-util-find-after": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", - "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -18086,8 +16556,6 @@ }, "node_modules/unist-util-remove-position": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", - "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", @@ -18136,17 +16604,14 @@ }, "node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.0.0" } }, "node_modules/unpipe": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -18158,9 +16623,8 @@ }, "node_modules/untildify": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -18254,593 +16718,232 @@ }, "node_modules/use-mcp": { "version": "0.0.18", - "resolved": "https://registry.npmjs.org/use-mcp/-/use-mcp-0.0.18.tgz", - "integrity": "sha512-4NU93NElE3XT5gqskaPp68Eot6aOtRedooaPXmfB4dVqnlH2MiS6Q1J+d1HNPbYTEC9aa/OJPkf53uZNN5vNEA==", "dependencies": { "@modelcontextprotocol/sdk": "^1.13.3", "strict-url-sanitise": "^0.0.1" } }, "node_modules/use-sync-external-store": { - "version": "1.4.0", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/util": { - "version": "0.12.5", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/uuid": { - "version": "9.0.1", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "devOptional": true, - "license": "MIT" - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vector_store_deployer": { - "resolved": "vector_store_deployer", - "link": true - }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "node_modules/vfile": { - "version": "6.0.3", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.2", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vite": { - "version": "6.3.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz", - "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==", - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vitepress": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", - "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==", - "license": "MIT", - "dependencies": { - "@docsearch/css": "3.8.2", - "@docsearch/js": "3.8.2", - "@iconify-json/simple-icons": "^1.2.21", - "@shikijs/core": "^2.1.0", - "@shikijs/transformers": "^2.1.0", - "@shikijs/types": "^2.1.0", - "@types/markdown-it": "^14.1.2", - "@vitejs/plugin-vue": "^5.2.1", - "@vue/devtools-api": "^7.7.0", - "@vue/shared": "^3.5.13", - "@vueuse/core": "^12.4.0", - "@vueuse/integrations": "^12.4.0", - "focus-trap": "^7.6.4", - "mark.js": "8.11.1", - "minisearch": "^7.1.1", - "shiki": "^2.1.0", - "vite": "^5.4.14", - "vue": "^3.5.13" - }, - "bin": { - "vitepress": "bin/vitepress.js" - }, - "peerDependencies": { - "markdown-it-mathjax3": "^4", - "postcss": "^8" - }, - "peerDependenciesMeta": { - "markdown-it-mathjax3": { - "optional": true - }, - "postcss": { - "optional": true - } - } - }, - "node_modules/vitepress/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitepress/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitepress/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitepress/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitepress/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitepress/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitepress/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitepress/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitepress/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitepress/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitepress/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitepress/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vitepress/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "version": "1.4.0", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/vitepress/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], + "node_modules/util": { + "version": "0.12.5", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" } }, - "node_modules/vitepress/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" + "node_modules/util-deprecate": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "9.0.1", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "bin": { + "uuid": "dist/bin/uuid" } }, - "node_modules/vitepress/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "devOptional": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, "engines": { - "node": ">=12" + "node": ">=10.12.0" } }, - "node_modules/vitepress/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], + "node_modules/vary": { + "version": "1.1.2", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">= 0.8" } }, - "node_modules/vitepress/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" + "node_modules/vector_store_deployer": { + "resolved": "vector_store_deployer", + "link": true + }, + "node_modules/verror": { + "version": "1.10.0", + "dev": true, + "engines": [ + "node >=0.6.0" ], "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" } }, - "node_modules/vitepress/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], + "node_modules/vfile": { + "version": "6.0.3", "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/vitepress/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], + "node_modules/vfile-message": { + "version": "4.0.2", "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/vitepress/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], + "node_modules/vite": { + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", + "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, "engines": { - "node": ">=12" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, - "node_modules/vitepress/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], + "node_modules/vitepress": { + "version": "1.6.4", "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@docsearch/css": "3.8.2", + "@docsearch/js": "3.8.2", + "@iconify-json/simple-icons": "^1.2.21", + "@shikijs/core": "^2.1.0", + "@shikijs/transformers": "^2.1.0", + "@shikijs/types": "^2.1.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/devtools-api": "^7.7.0", + "@vue/shared": "^3.5.13", + "@vueuse/core": "^12.4.0", + "@vueuse/integrations": "^12.4.0", + "focus-trap": "^7.6.4", + "mark.js": "8.11.1", + "minisearch": "^7.1.1", + "shiki": "^2.1.0", + "vite": "^5.4.14", + "vue": "^3.5.13" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } } }, - "node_modules/vitepress/node_modules/@esbuild/win32-x64": { + "node_modules/vitepress/node_modules/@esbuild/linux-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { "node": ">=12" @@ -18848,8 +16951,6 @@ }, "node_modules/vitepress/node_modules/@vueuse/integrations": { "version": "12.8.2", - "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz", - "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==", "license": "MIT", "dependencies": { "@vueuse/core": "12.8.2", @@ -18914,8 +17015,6 @@ }, "node_modules/vitepress/node_modules/esbuild": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -18952,8 +17051,6 @@ }, "node_modules/vitepress/node_modules/jwt-decode": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", - "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", "license": "MIT", "optional": true, "peer": true, @@ -19022,8 +17119,6 @@ }, "node_modules/vscode-jsonrpc": { "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", - "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -19031,8 +17126,6 @@ }, "node_modules/vscode-languageserver": { "version": "9.0.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", - "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", "license": "MIT", "dependencies": { "vscode-languageserver-protocol": "3.17.5" @@ -19043,8 +17136,6 @@ }, "node_modules/vscode-languageserver-protocol": { "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", - "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", "license": "MIT", "dependencies": { "vscode-jsonrpc": "8.2.0", @@ -19053,26 +17144,18 @@ }, "node_modules/vscode-languageserver-textdocument": { "version": "1.0.12", - "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", - "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", "license": "MIT" }, "node_modules/vscode-languageserver-types": { "version": "3.17.5", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", - "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", "license": "MIT" }, "node_modules/vscode-uri": { "version": "3.0.8", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", - "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", "license": "MIT" }, "node_modules/vue": { "version": "3.5.16", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.16.tgz", - "integrity": "sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w==", "license": "MIT", "dependencies": { "@vue/compiler-dom": "3.5.16", @@ -19092,9 +17175,8 @@ }, "node_modules/wait-on": { "version": "8.0.3", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.3.tgz", - "integrity": "sha512-nQFqAFzZDeRxsu7S3C7LbuxslHhk+gnJZHyethuGKAn2IVleIbTB9I3vJSQiSR+DifUqmdzfPMoMPJfLqMF2vw==", "dev": true, + "license": "MIT", "dependencies": { "axios": "^1.8.2", "joi": "^17.13.3", @@ -19239,8 +17321,6 @@ }, "node_modules/wicked-good-xpath": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz", - "integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==", "license": "MIT" }, "node_modules/word-wrap": { @@ -19360,8 +17440,6 @@ }, "node_modules/ws": { "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", "license": "MIT", "optional": true, "peer": true, @@ -19401,8 +17479,6 @@ }, "node_modules/xtend": { "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "license": "MIT", "engines": { "node": ">=0.4" @@ -19423,8 +17499,6 @@ }, "node_modules/yaml": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -19494,9 +17568,8 @@ }, "node_modules/yauzl": { "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, + "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" @@ -19537,8 +17610,6 @@ }, "node_modules/zod2md": { "version": "0.1.9", - "resolved": "https://registry.npmjs.org/zod2md/-/zod2md-0.1.9.tgz", - "integrity": "sha512-HkdAQkWAGRbybDtcHrHdchuUzLjaNMWL9Rbwwde2G7CKkn12oa/J/Fk4sHUQd21U0EiMco2bfWWJpAibegI2Lg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 9f66eb3e3..99dcedbfe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@awslabs/lisa", - "version": "5.2.0", + "version": "5.3.0", "description": "A scalable infrastructure-as-code solution for self-hosting and orchestrating LLM inference with RAG capabilities, providing low-latency access to generative AI and embedding models across multiple providers.", "homepage": "https://awslabs.github.io/LISA/", "license": "Apache-2.0", diff --git a/pyproject.toml b/pyproject.toml index 4930071a7..8f09dcbf1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ strict_optional = true show_error_codes = true [tool.ruff] -ignore = ["D401"] +lint.ignore = ["D401"] line-length = 120 [tool.pytest.ini_options] diff --git a/requirements-dev.txt b/requirements-dev.txt index 382c27126..4533e6e59 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -26,7 +26,7 @@ lxml==5.1.0 opensearch-py==2.8.0 requests_aws4auth==1.3.1 PyJWT==2.8.0 -psycopg2-binary==2.9.9 +psycopg2-binary==2.9.10 # Development black==24.3.0 diff --git a/test/cdk/stacks/nag.test.ts b/test/cdk/stacks/nag.test.ts index 95686c1c4..410b01e31 100644 --- a/test/cdk/stacks/nag.test.ts +++ b/test/cdk/stacks/nag.test.ts @@ -31,7 +31,7 @@ enum NagType { } const nagResults: NagResult = { - LisaApiBase: [1,7,0,7], + LisaApiBase: [1,27,0,25], LisaApiDeployment: [0,0,0,0], LisaChat: [4,62,0,69], LisaCore: [0,1,0,6], @@ -40,7 +40,7 @@ const nagResults: NagResult = { LisaModels: [1,77,0,64], LisaNetworking: [1,2,3,5], LisaRAG: [3,51,0,50], - LisaServe: [1,24,0,32], + LisaServe: [1,36,0,37], LisaUI: [0,15,0,7], LisaMetrics: [1,11,0,12] }; diff --git a/test/cdk/stacks/roleOverrides.test.ts b/test/cdk/stacks/roleOverrides.test.ts index 763842a0e..1f26241c8 100644 --- a/test/cdk/stacks/roleOverrides.test.ts +++ b/test/cdk/stacks/roleOverrides.test.ts @@ -21,8 +21,8 @@ import { Roles } from '../../../lib/core/iam/roles'; import { Stack } from 'aws-cdk-lib'; const stackRolesOverrides: Record = { - 'LisaApiBase': 1, - 'LisaServe': 4, + 'LisaApiBase': 4, + 'LisaServe': 5, 'LisaUI': 1, 'LisaDocs': 2, 'LisaRAG': 4, @@ -31,8 +31,8 @@ const stackRolesOverrides: Record = { }; const stackRoles: Record = { - 'LisaApiBase': 2, - 'LisaServe': 5, + 'LisaApiBase': 5, + 'LisaServe': 7, 'LisaUI': 3, 'LisaNetworking': 0, 'LisaChat': 6, diff --git a/test/lambda/conftest.py b/test/lambda/conftest.py new file mode 100644 index 000000000..027c40482 --- /dev/null +++ b/test/lambda/conftest.py @@ -0,0 +1,21 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + + +@pytest.fixture +def sample_jwt_data(): + """Create a sample JWT data.""" + return {"sub": "test-user-id", "username": "test-user", "groups": ["test-group"], "nested": {"property": "value"}} diff --git a/test/lambda/test_authorizer_lambda.py b/test/lambda/test_authorizer_lambda.py index 1d4c2b878..177eea52a 100644 --- a/test/lambda/test_authorizer_lambda.py +++ b/test/lambda/test_authorizer_lambda.py @@ -97,7 +97,6 @@ def wrapper(*args, **kwargs): find_jwt_username, generate_policy, get_management_tokens, - get_property_path, id_token_is_valid, is_admin, is_valid_api_token, @@ -200,24 +199,6 @@ def test_find_jwt_username(sample_jwt_data): assert "No username found in JWT" in str(excinfo.value) -def test_get_property_path(sample_jwt_data): - """Test the get_property_path function.""" - # Test with simple property - assert get_property_path(sample_jwt_data, "username") == "test-user" - - # Test with nested property - assert get_property_path(sample_jwt_data, "nested.property") == "value" - - # Test with non-existent property - assert get_property_path(sample_jwt_data, "nonexistent") is None - - # Test with non-existent nested property - assert get_property_path(sample_jwt_data, "nested.nonexistent") is None - - # Test with non-existent parent - assert get_property_path(sample_jwt_data, "nonexistent.property") is None - - def test_is_admin(sample_jwt_data): """Test the is_admin function.""" # Test when user is admin diff --git a/test/lambda/test_common_functions.py b/test/lambda/test_common_functions.py index 88d9cc824..e678628af 100644 --- a/test/lambda/test_common_functions.py +++ b/test/lambda/test_common_functions.py @@ -12,14 +12,32 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - import json import os import sys from decimal import Decimal import pytest +from utilities.common_functions import get_property_path + + +def test_get_property_path(sample_jwt_data): + """Test the get_property_path function.""" + # Test with simple property + assert get_property_path(sample_jwt_data, "username") == "test-user" + + # Test with nested property + assert get_property_path(sample_jwt_data, "nested.property") == "value" + + # Test with non-existent property + assert get_property_path(sample_jwt_data, "nonexistent") is None + + # Test with non-existent nested property + assert get_property_path(sample_jwt_data, "nested.nonexistent") is None + + # Test with non-existent parent + assert get_property_path(sample_jwt_data, "nonexistent.property") is None + # Add the lambda directory to the Python path sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../")) diff --git a/test/lambda/test_create_model_state_machine.py b/test/lambda/test_create_model_state_machine.py index d4c3406bc..3ed05f42d 100644 --- a/test/lambda/test_create_model_state_machine.py +++ b/test/lambda/test_create_model_state_machine.py @@ -112,7 +112,9 @@ class ImageNotFoundException(Exception): # Create comprehensive mock for boto3.client to handle all possible service requests -def mock_boto3_client(service, **kwargs): +def mock_boto3_client(*args, **kwargs): + # Support both (service_name, region_name, config) and (service_name) + service = args[0] if args else kwargs.get("service_name", kwargs.get("service")) if service == "lambda": return mock_lambda elif service == "ecr": @@ -139,6 +141,7 @@ def mock_boto3_client(service, **kwargs): return MagicMock() +# Note: This module needs global boto3.client patch for import-time dependencies patch("boto3.client", side_effect=mock_boto3_client).start() # Patch the specific clients in the state machine module diff --git a/test/lambda/test_delete_model_state_machine.py b/test/lambda/test_delete_model_state_machine.py index 8344016cc..17e37ad62 100644 --- a/test/lambda/test_delete_model_state_machine.py +++ b/test/lambda/test_delete_model_state_machine.py @@ -76,7 +76,9 @@ # Create comprehensive mock for boto3.client to handle all possible service requests -def mock_boto3_client(service, **kwargs): +def mock_boto3_client(*args, **kwargs): + # Support both (service_name, region_name, config) and (service_name) + service = args[0] if args else kwargs.get("service_name", kwargs.get("service")) if service == "cloudformation": return mock_cfn elif service == "iam": @@ -97,6 +99,7 @@ def mock_boto3_client(service, **kwargs): return MagicMock() +# Note: This module needs global boto3.client patch for import-time dependencies patch("boto3.client", side_effect=mock_boto3_client).start() from models.domain_objects import ModelStatus diff --git a/test/lambda/test_mcp_server_lambda.py b/test/lambda/test_mcp_server_lambda.py index b31b4d141..9d6a665c4 100644 --- a/test/lambda/test_mcp_server_lambda.py +++ b/test/lambda/test_mcp_server_lambda.py @@ -386,7 +386,7 @@ def test_create_mcp_server_with_owner(mcp_servers_table, lambda_context): response = create(event, lambda_context) assert response["statusCode"] == 200 body = json.loads(response["body"]) - assert body["owner"] == "custom-owner" + assert body["owner"] == "test-user" def test_update_mcp_server_success(mcp_servers_table, sample_mcp_server, lambda_context): @@ -705,3 +705,85 @@ def test_get_mcp_server_global_non_owner_access(mcp_servers_table, sample_global # Reset mock mock_common.get_username.return_value = "test-user" + + +def test_get_mcp_servers_groups_filtering(mcp_servers_table, lambda_context): + """Test groups filtering logic in _get_mcp_servers function.""" + from mcp_server.lambda_functions import _get_mcp_servers + + # Create test servers with different group configurations + test_servers = [ + # Server owned by user with no groups - should be included + {"id": "server1", "owner": "test-user", "status": "active", "groups": None}, + # Server owned by user with matching groups - should be included + {"id": "server2", "owner": "test-user", "status": "active", "groups": ["group:admin", "group:user"]}, + # Server owned by user with non-matching groups - should be excluded + {"id": "server3", "owner": "test-user", "status": "active", "groups": ["group:other"]}, + # Public server with no groups - should be included + {"id": "server4", "owner": "lisa:public", "status": "active", "groups": None}, + # Public server with matching groups - should be included + {"id": "server5", "owner": "lisa:public", "status": "active", "groups": ["group:admin"]}, + # Public server with non-matching groups - should be excluded + {"id": "server6", "owner": "lisa:public", "status": "active", "groups": ["group:other"]}, + # Inactive server - should be excluded + {"id": "server7", "owner": "test-user", "status": "inactive", "groups": ["group:admin"]}, + ] + + # Insert test servers into the table + for server in test_servers: + mcp_servers_table.put_item(Item=server) + + # Test with groups filter + result = _get_mcp_servers(user_id="test-user", active=True, groups=["admin", "user"]) + + # Should include: server1 (user owns, no groups), server2 (user owns, matching groups), + # server3 (user owns, non-matching groups), server4 (public, no groups), server5 (public, matching groups) + # Should exclude: server6 (public, non-matching groups), server7 (inactive) + expected_ids = {"server1", "server2", "server3", "server4", "server5"} + actual_ids = {item["id"] for item in result["Items"]} + + assert actual_ids == expected_ids, f"Expected {expected_ids}, got {actual_ids}" + + +def test_get_mcp_servers_no_groups_filter(mcp_servers_table, lambda_context): + """Test behavior when no groups are provided.""" + from mcp_server.lambda_functions import _get_mcp_servers + + test_servers = [ + {"id": "server1", "owner": "test-user", "status": "active", "groups": None}, + {"id": "server2", "owner": "test-user", "status": "active", "groups": ["group:admin"]}, + ] + + for server in test_servers: + mcp_servers_table.put_item(Item=server) + + # Test without groups filter + result = _get_mcp_servers(user_id="test-user", active=True, groups=None) + + # Should include all servers (no groups filtering) + expected_ids = {"server1", "server2"} + actual_ids = {item["id"] for item in result["Items"]} + + assert actual_ids == expected_ids + + +def test_get_mcp_servers_empty_groups_filter(mcp_servers_table, lambda_context): + """Test behavior when empty groups list is provided.""" + from mcp_server.lambda_functions import _get_mcp_servers + + test_servers = [ + {"id": "server1", "owner": "test-user", "status": "active", "groups": None}, + {"id": "server2", "owner": "test-user", "status": "active", "groups": ["group:admin"]}, + ] + + for server in test_servers: + mcp_servers_table.put_item(Item=server) + + # Test with empty groups list + result = _get_mcp_servers(user_id="test-user", active=True, groups=[]) + + # Should include all servers (empty groups list means no filtering) + expected_ids = {"server1", "server2"} + actual_ids = {item["id"] for item in result["Items"]} + + assert actual_ids == expected_ids diff --git a/test/lambda/test_mcp_workbench_lambda.py b/test/lambda/test_mcp_workbench_lambda.py new file mode 100644 index 000000000..b192a99bb --- /dev/null +++ b/test/lambda/test_mcp_workbench_lambda.py @@ -0,0 +1,806 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for MCP workbench lambda functions.""" + +import functools +import json +import logging +import os +import sys +from datetime import datetime +from types import SimpleNamespace +from unittest.mock import patch + +import botocore.exceptions +import pytest +from botocore.config import Config +from moto import mock_aws + +# Add the lambda directory to the Python path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../")) + +# Set up mock AWS credentials and environment variables before importing +os.environ["AWS_ACCESS_KEY_ID"] = "testing" +os.environ["AWS_SECRET_ACCESS_KEY"] = "testing" +os.environ["AWS_SECURITY_TOKEN"] = "testing" +os.environ["AWS_SESSION_TOKEN"] = "testing" +os.environ["AWS_DEFAULT_REGION"] = "us-east-1" +os.environ["AWS_REGION"] = "us-east-1" +os.environ["WORKBENCH_BUCKET"] = "workbench-bucket" + +mock_env = { + "AWS_ACCESS_KEY_ID": "testing", + "AWS_SECRET_ACCESS_KEY": "testing", + "AWS_SECURITY_TOKEN": "testing", + "AWS_SESSION_TOKEN": "testing", + "AWS_DEFAULT_REGION": "us-east-1", + "AWS_REGION": "us-east-1", + "WORKBENCH_BUCKET": "workbench-bucket", +} + +# Create a real retry config +retry_config = Config(retries=dict(max_attempts=3), defaults_mode="standard") + +# Define the bucket name used in tests +WORKBENCH_BUCKET = "workbench-bucket" + + +def mock_api_wrapper(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + try: + result = func(*args, **kwargs) + if isinstance(result, dict) and "statusCode" in result: + return result + return { + "statusCode": 200, + "headers": {"Content-Type": "application/json", "Access-Control-Allow-Origin": "*"}, + "body": json.dumps(result, default=str), + } + except Exception as e: + logging.error(f"Error in {func.__name__}: {str(e)}") + return { + "statusCode": 500, + "headers": {"Content-Type": "application/json", "Access-Control-Allow-Origin": "*"}, + "body": json.dumps({"error": str(e)}), + } + + return wrapper + + +# Create mock modules for patching with complete isolation +@pytest.fixture(autouse=True, scope="function") +def mock_common(): + """Ensure complete test isolation with fresh environment.""" + with patch.dict("os.environ", mock_env, clear=True): + # Reset any module-level variables that might be cached + yield + + +@pytest.fixture +def lambda_context(): + """Create a mock Lambda context.""" + return SimpleNamespace( + function_name="test_function", + function_version="$LATEST", + invoked_function_arn="arn:aws:lambda:us-east-1:123456789012:function:test_function", + memory_limit_in_mb=128, + aws_request_id="test-request-id", + log_group_name="/aws/lambda/test_function", + log_stream_name="2024/03/27/[$LATEST]test123", + ) + + +SAMPLE_TOOL_CONTENT = """ +def hello_world(): + print("Hello, world!") + return "Hello from MCP Tool" +""" + +SAMPLE_TOOL_ID = "test_tool.py" + + +@pytest.fixture +def s3_setup(): + """Set up S3 with moto and create bucket. Uses complete isolation to avoid test interference.""" + # More aggressive approach: Temporarily replace boto3.client entirely + import importlib + + import boto3 + + # Save original boto3.client + original_boto3_client = boto3.client + + try: + # Create a completely isolated moto context + with mock_aws(): + # Force a fresh import of boto3 within the moto context + importlib.reload(boto3) + + # Create a fresh S3 client + s3_client = boto3.client("s3", region_name="us-east-1") + + # Create the bucket + s3_client.create_bucket(Bucket=WORKBENCH_BUCKET) + + # Create a storage dict to track what we put in S3 during tests + # This helps with debugging if moto state gets corrupted + s3_storage = {} + + # Wrap the original methods to track operations + original_put_object = s3_client.put_object + original_get_object = s3_client.get_object + original_delete_object = s3_client.delete_object + original_list_objects_v2 = s3_client.list_objects_v2 + + def tracked_put_object(**kwargs): + key = kwargs.get("Key") + body = kwargs.get("Body") + if isinstance(body, bytes): + s3_storage[key] = body.decode("utf-8") + else: + s3_storage[key] = str(body) + return original_put_object(**kwargs) + + def tracked_get_object(**kwargs): + return original_get_object(**kwargs) + + def tracked_delete_object(**kwargs): + key = kwargs.get("Key") + if key in s3_storage: + del s3_storage[key] + return original_delete_object(**kwargs) + + def tracked_list_objects_v2(**kwargs): + return original_list_objects_v2(**kwargs) + + s3_client.put_object = tracked_put_object + s3_client.get_object = tracked_get_object + s3_client.delete_object = tracked_delete_object + s3_client.list_objects_v2 = tracked_list_objects_v2 + + yield s3_client + finally: + # Restore original boto3.client + boto3.client = original_boto3_client + + +# Test the MCPToolModel directly without mocking dependencies +def test_mcp_tool_model(): + """Test the MCPToolModel class.""" + # Import inside the test to avoid import-time patching issues + from mcp_workbench.lambda_functions import MCPToolModel + + # Test with .py extension + tool = MCPToolModel(id="test_tool.py", contents="print('hello')") + assert tool.s3_key == "test_tool.py" + + # Test without .py extension + tool = MCPToolModel(id="test_tool", contents="print('hello')") + assert tool.s3_key == "test_tool.py" + + # Test with updated_at + timestamp = datetime.now().isoformat() + tool = MCPToolModel(id="test_tool.py", contents="print('hello')", updated_at=timestamp) + assert tool.updated_at == timestamp + + +# Test CRUD operations with moto +def test_get_tool_from_s3(s3_setup): + """Test retrieving a tool from S3.""" + # Upload a file to the mocked S3 + s3_setup.put_object( + Bucket=WORKBENCH_BUCKET, + Key=SAMPLE_TOOL_ID, + Body=SAMPLE_TOOL_CONTENT.encode("utf-8"), + ContentType="text/x-python", + ) + + # Import and test the function + from mcp_workbench.lambda_functions import _get_tool_from_s3 + + # Use the actual function with moto S3 + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "mcp_workbench.lambda_functions.WORKBENCH_BUCKET", WORKBENCH_BUCKET + ): + tool = _get_tool_from_s3(SAMPLE_TOOL_ID) + + # Verify tool properties + assert tool.id == SAMPLE_TOOL_ID + assert tool.contents == SAMPLE_TOOL_CONTENT + + +def test_get_tool_from_s3_not_found(s3_setup): + """Test retrieving a non-existent tool from S3.""" + # Import and test the function + from mcp_workbench.lambda_functions import _get_tool_from_s3 + + # Test retrieving non-existent tool with moto S3 + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "mcp_workbench.lambda_functions.WORKBENCH_BUCKET", WORKBENCH_BUCKET + ): + with pytest.raises(Exception) as excinfo: + _get_tool_from_s3("non_existent_tool.py") + assert "not found" in str(excinfo.value).lower() + + +def test_get_tool_from_s3_adds_py_extension(s3_setup): + """Test retrieving a tool without .py extension.""" + # Upload a file to the mocked S3 with .py extension + s3_setup.put_object( + Bucket=WORKBENCH_BUCKET, + Key="test_tool.py", + Body=SAMPLE_TOOL_CONTENT.encode("utf-8"), + ContentType="text/x-python", + ) + + # Import and test the function + from mcp_workbench.lambda_functions import _get_tool_from_s3 + + # Use the actual function with moto S3, but request without .py extension + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "mcp_workbench.lambda_functions.WORKBENCH_BUCKET", WORKBENCH_BUCKET + ): + tool = _get_tool_from_s3("test_tool") + + assert tool.id == "test_tool.py" + assert tool.contents == SAMPLE_TOOL_CONTENT + + +def test_read_success(s3_setup, lambda_context): + """Test successful retrieval of tool.""" + # Upload a file to the mocked S3 + s3_setup.put_object( + Bucket=WORKBENCH_BUCKET, + Key=SAMPLE_TOOL_ID, + Body=SAMPLE_TOOL_CONTENT.encode("utf-8"), + ContentType="text/x-python", + ) + + # Create the event + event = { + "requestContext": {"authorizer": {"username": "test-admin"}}, + "pathParameters": {"toolId": SAMPLE_TOOL_ID}, + } + + # Import and test the function + from mcp_workbench.lambda_functions import read + + # Use the actual function with moto S3 + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "mcp_workbench.lambda_functions.is_admin", return_value=True + ), patch("mcp_workbench.lambda_functions.WORKBENCH_BUCKET", WORKBENCH_BUCKET): + response = read(event, lambda_context) + + assert response["statusCode"] == 200 + body = json.loads(response["body"]) + assert body["id"] == SAMPLE_TOOL_ID + assert body["contents"] == SAMPLE_TOOL_CONTENT + + +def test_read_not_admin(s3_setup, lambda_context): + """Test unauthorized retrieval of tool.""" + # Create the event + event = { + "requestContext": {"authorizer": {"username": "regular-user"}}, + "pathParameters": {"toolId": SAMPLE_TOOL_ID}, + } + + # Import and test the function + from mcp_workbench.lambda_functions import read + + # Use the actual function with moto S3 and patched is_admin + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "utilities.common_functions.get_username", return_value="regular-user" + ), patch("mcp_workbench.lambda_functions.api_wrapper", mock_api_wrapper): + response = read(event, lambda_context) + + assert response["statusCode"] == 400 + body = json.loads(response["body"]) + # Handle both string and dict response formats + error_text = body if isinstance(body, str) else body.get("error", "") + assert "Only admin users can access tools" in error_text + + +def test_read_not_found(s3_setup, lambda_context): + """Test reading a non-existent tool.""" + # Create the event for a non-existent tool + event = { + "requestContext": {"authorizer": {"username": "test-admin"}}, + "pathParameters": {"toolId": "non_existent_tool.py"}, + } + + # Import and test the function + from mcp_workbench.lambda_functions import read + + # Use the actual function with moto S3 + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "mcp_workbench.lambda_functions.is_admin", return_value=True + ), patch("mcp_workbench.lambda_functions.WORKBENCH_BUCKET", WORKBENCH_BUCKET): + response = read(event, lambda_context) + + assert response["statusCode"] == 500 + body = json.loads(response["body"]) + # Handle both string and dict response formats + error_text = body if isinstance(body, str) else body.get("error", "") + assert "not found" in error_text.lower() + + +def test_read_missing_tool_id(s3_setup, lambda_context): + """Test reading without a toolId parameter.""" + # Create the event with missing pathParameters + event = { + "requestContext": {"authorizer": {"username": "test-admin"}}, + "pathParameters": {}, + } + + # Import and test the function + from mcp_workbench.lambda_functions import read + + # Use the actual function with moto S3 + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "mcp_workbench.lambda_functions.is_admin", return_value=True + ): + response = read(event, lambda_context) + + assert response["statusCode"] == 400 + body = json.loads(response["body"]) + # Handle both string and dict response formats + error_text = body if isinstance(body, str) else body.get("error", "") + assert "Missing toolId parameter" in error_text + + +def test_list_success(s3_setup, lambda_context): + """Test successful listing of tools.""" + # Upload files to the mocked S3 + s3_setup.put_object( + Bucket=WORKBENCH_BUCKET, Key="tool1.py", Body=SAMPLE_TOOL_CONTENT.encode("utf-8"), ContentType="text/x-python" + ) + s3_setup.put_object( + Bucket=WORKBENCH_BUCKET, Key="tool2.py", Body=SAMPLE_TOOL_CONTENT.encode("utf-8"), ContentType="text/x-python" + ) + s3_setup.put_object( + Bucket=WORKBENCH_BUCKET, Key="not_a_tool.txt", Body=b"This is not a python file", ContentType="text/plain" + ) + + # Create the event + event = {"requestContext": {"authorizer": {"username": "test-admin"}}} + + # Import and test the function + from mcp_workbench.lambda_functions import list as list_tools + + # Use the actual function with moto S3 + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "mcp_workbench.lambda_functions.is_admin", return_value=True + ), patch("mcp_workbench.lambda_functions.WORKBENCH_BUCKET", WORKBENCH_BUCKET): + response = list_tools(event, lambda_context) + + assert response["statusCode"] == 200 + body = json.loads(response["body"]) + assert "tools" in body + assert len(body["tools"]) == 2 # Only the .py files + + # Verify the tool ids + tool_ids = [tool["id"] for tool in body["tools"]] + assert "tool1.py" in tool_ids + assert "tool2.py" in tool_ids + assert "not_a_tool.txt" not in tool_ids + + +def test_list_not_admin(s3_setup, lambda_context): + """Test unauthorized listing of tools.""" + # Create the event + event = {"requestContext": {"authorizer": {"username": "regular-user"}}} + + # Import and test the function + from mcp_workbench.lambda_functions import list as list_tools + + # Use the actual function with moto S3 and patched is_admin + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "utilities.common_functions.get_username", return_value="regular-user" + ), patch("mcp_workbench.lambda_functions.api_wrapper", mock_api_wrapper): + response = list_tools(event, lambda_context) + + assert response["statusCode"] == 400 + body = json.loads(response["body"]) + # Handle both string and dict response formats + error_text = body if isinstance(body, str) else body.get("error", "") + assert "Only admin users can access tools" in error_text + + +def test_list_empty_bucket(s3_setup, lambda_context): + """Test listing tools in an empty bucket.""" + # Create the event (bucket is already empty from s3_setup) + event = {"requestContext": {"authorizer": {"username": "test-admin"}}} + + # Import and test the function + from mcp_workbench.lambda_functions import list as list_tools + + # Use the actual function with moto S3 + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "mcp_workbench.lambda_functions.is_admin", return_value=True + ), patch("mcp_workbench.lambda_functions.WORKBENCH_BUCKET", WORKBENCH_BUCKET): + response = list_tools(event, lambda_context) + + assert response["statusCode"] == 200 + body = json.loads(response["body"]) + assert "tools" in body + assert len(body["tools"]) == 0 + + +def test_create_success(s3_setup, lambda_context): + """Test successful creation of a tool.""" + # Create the event + event = { + "requestContext": {"authorizer": {"username": "test-admin"}}, + "body": json.dumps({"id": "new_tool.py", "contents": SAMPLE_TOOL_CONTENT}), + } + + # Import and test the function + from mcp_workbench.lambda_functions import create + + # Use the actual function with moto S3 + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "mcp_workbench.lambda_functions.is_admin", return_value=True + ), patch("mcp_workbench.lambda_functions.WORKBENCH_BUCKET", WORKBENCH_BUCKET): + response = create(event, lambda_context) + + assert response["statusCode"] == 200 + body = json.loads(response["body"]) + assert body["id"] == "new_tool.py" + assert body["contents"] == SAMPLE_TOOL_CONTENT + + # Verify the object was actually created in S3 + s3_obj = s3_setup.get_object(Bucket=WORKBENCH_BUCKET, Key="new_tool.py") + created_content = s3_obj["Body"].read().decode("utf-8") + assert created_content == SAMPLE_TOOL_CONTENT + + +def test_create_without_py_extension(s3_setup, lambda_context): + """Test creating a tool without .py extension.""" + # Create the event + event = { + "requestContext": {"authorizer": {"username": "test-admin"}}, + "body": json.dumps({"id": "new_tool", "contents": SAMPLE_TOOL_CONTENT}), + } + + # Import and test the function + from mcp_workbench.lambda_functions import create + + # Use the actual function with moto S3 + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "mcp_workbench.lambda_functions.is_admin", return_value=True + ), patch("mcp_workbench.lambda_functions.WORKBENCH_BUCKET", WORKBENCH_BUCKET): + response = create(event, lambda_context) + + assert response["statusCode"] == 200 + body = json.loads(response["body"]) + assert body["id"] == "new_tool" + + # Verify the object was actually created in S3 with .py extension + s3_obj = s3_setup.get_object(Bucket=WORKBENCH_BUCKET, Key="new_tool.py") + created_content = s3_obj["Body"].read().decode("utf-8") + assert created_content == SAMPLE_TOOL_CONTENT + + +def test_create_not_admin(s3_setup, lambda_context): + """Test unauthorized creation of a tool.""" + # Create the event + event = { + "requestContext": {"authorizer": {"username": "regular-user"}}, + "body": json.dumps({"id": "unauthorized_tool.py", "contents": SAMPLE_TOOL_CONTENT}), + } + + # Import and test the function + from mcp_workbench.lambda_functions import create + + # Use the actual function with moto S3 and patched is_admin + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "utilities.common_functions.get_username", return_value="regular-user" + ), patch("mcp_workbench.lambda_functions.api_wrapper", mock_api_wrapper): + response = create(event, lambda_context) + + assert response["statusCode"] == 400 + body = json.loads(response["body"]) + # Handle both string and dict response formats + error_text = body if isinstance(body, str) else body.get("error", "") + assert "Only admin users can access tools" in error_text + + +def test_create_missing_fields(s3_setup, lambda_context): + """Test creating a tool with missing required fields.""" + # Create the event with missing contents + event = { + "requestContext": {"authorizer": {"username": "test-admin"}}, + "body": json.dumps({"id": "incomplete_tool.py"}), + } + + # Import and test the function + from mcp_workbench.lambda_functions import create + + # Use the actual function with moto S3 + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "mcp_workbench.lambda_functions.is_admin", return_value=True + ): + response = create(event, lambda_context) + + assert response["statusCode"] == 400 + body = json.loads(response["body"]) + # Handle both string and dict response formats + error_text = body if isinstance(body, str) else body.get("error", "") + assert "Missing required fields" in error_text + + +def test_update_success(s3_setup, lambda_context): + """Test successful update of a tool.""" + # Upload initial file to the mocked S3 + s3_setup.put_object( + Bucket=WORKBENCH_BUCKET, + Key=SAMPLE_TOOL_ID, + Body=SAMPLE_TOOL_CONTENT.encode("utf-8"), + ContentType="text/x-python", + ) + + # Updated content + updated_content = """ +def updated_function(): + return "This is the updated function" +""" + + # Create the event + event = { + "requestContext": {"authorizer": {"username": "test-admin"}}, + "pathParameters": {"toolId": SAMPLE_TOOL_ID}, + "body": json.dumps({"contents": updated_content}), + } + + # Import and test the function + from mcp_workbench.lambda_functions import update + + # Use the actual function with moto S3 + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "mcp_workbench.lambda_functions.is_admin", return_value=True + ), patch("mcp_workbench.lambda_functions.WORKBENCH_BUCKET", WORKBENCH_BUCKET): + response = update(event, lambda_context) + + assert response["statusCode"] == 200 + body = json.loads(response["body"]) + assert body["id"] == SAMPLE_TOOL_ID + assert body["contents"] == updated_content + + # Verify the object was actually updated in S3 + s3_obj = s3_setup.get_object(Bucket=WORKBENCH_BUCKET, Key=SAMPLE_TOOL_ID) + updated_content_from_s3 = s3_obj["Body"].read().decode("utf-8") + assert updated_content_from_s3 == updated_content + + +def test_update_not_admin(s3_setup, lambda_context): + """Test unauthorized update of a tool.""" + # Create the event + event = { + "requestContext": {"authorizer": {"username": "regular-user"}}, + "pathParameters": {"toolId": SAMPLE_TOOL_ID}, + "body": json.dumps({"contents": "Updated content"}), + } + + # Import and test the function + from mcp_workbench.lambda_functions import update + + # Use the actual function with moto S3 and patched is_admin + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "utilities.common_functions.get_username", return_value="regular-user" + ), patch("mcp_workbench.lambda_functions.api_wrapper", mock_api_wrapper): + response = update(event, lambda_context) + + assert response["statusCode"] == 400 + body = json.loads(response["body"]) + # Handle both string and dict response formats + error_text = body if isinstance(body, str) else body.get("error", "") + assert "Only admin users can access tools" in error_text + + +def test_update_not_found(s3_setup, lambda_context): + """Test updating a non-existent tool.""" + # Create the event for a non-existent tool + event = { + "requestContext": {"authorizer": {"username": "test-admin"}}, + "pathParameters": {"toolId": "non_existent_tool.py"}, + "body": json.dumps({"contents": "Updated content"}), + } + + # Import and test the function + from mcp_workbench.lambda_functions import update + + # Use the actual function with moto S3 + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "mcp_workbench.lambda_functions.is_admin", return_value=True + ), patch("mcp_workbench.lambda_functions.WORKBENCH_BUCKET", WORKBENCH_BUCKET): + response = update(event, lambda_context) + + assert response["statusCode"] == 400 + body = json.loads(response["body"]) + # Handle both string and dict response formats + error_text = body if isinstance(body, str) else body.get("error", "") + assert "does not exist" in error_text + + +def test_update_missing_tool_id(s3_setup, lambda_context): + """Test updating without a toolId parameter.""" + # Create the event with missing pathParameters + event = { + "requestContext": {"authorizer": {"username": "test-admin"}}, + "pathParameters": {}, + "body": json.dumps({"contents": "Updated content"}), + } + + # Import and test the function + from mcp_workbench.lambda_functions import update + + # Use the actual function with moto S3 + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "mcp_workbench.lambda_functions.is_admin", return_value=True + ): + response = update(event, lambda_context) + + assert response["statusCode"] == 400 + body = json.loads(response["body"]) + # Handle both string and dict response formats + error_text = body if isinstance(body, str) else body.get("error", "") + assert "Missing toolId parameter" in error_text + + +def test_update_missing_contents(s3_setup, lambda_context): + """Test updating a tool without contents.""" + # Upload initial file to the mocked S3 + s3_setup.put_object( + Bucket=WORKBENCH_BUCKET, + Key=SAMPLE_TOOL_ID, + Body=SAMPLE_TOOL_CONTENT.encode("utf-8"), + ContentType="text/x-python", + ) + + # Create the event with missing contents + event = { + "requestContext": {"authorizer": {"username": "test-admin"}}, + "pathParameters": {"toolId": SAMPLE_TOOL_ID}, + "body": json.dumps({}), + } + + # Import and test the function + from mcp_workbench.lambda_functions import update + + # Use the actual function with moto S3 + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "mcp_workbench.lambda_functions.is_admin", return_value=True + ): + response = update(event, lambda_context) + + assert response["statusCode"] == 400 + body = json.loads(response["body"]) + # Handle both string and dict response formats + error_text = body if isinstance(body, str) else body.get("error", "") + assert "Missing required field: 'contents'" in error_text + + +# Test delete operations with moto +def test_delete_success(s3_setup, lambda_context): + """Test successful deletion of a tool.""" + # Upload a file to the mocked S3 + s3_setup.put_object( + Bucket=WORKBENCH_BUCKET, + Key=SAMPLE_TOOL_ID, + Body=SAMPLE_TOOL_CONTENT.encode("utf-8"), + ContentType="text/x-python", + ) + + # Create the event + event = { + "requestContext": {"authorizer": {"username": "test-admin"}}, + "pathParameters": {"toolId": SAMPLE_TOOL_ID}, + } + + # Import and test the function + from mcp_workbench.lambda_functions import delete + + # Use the actual function with moto S3 + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "mcp_workbench.lambda_functions.is_admin", return_value=True + ), patch("mcp_workbench.lambda_functions.WORKBENCH_BUCKET", WORKBENCH_BUCKET): + response = delete(event, lambda_context) + + assert response["statusCode"] == 200 + body = json.loads(response["body"]) + assert "status" in body + assert body["status"] == "ok" + assert f"Tool {SAMPLE_TOOL_ID} deleted successfully" in body["message"] + + # Verify the object was actually deleted from S3 + try: + s3_setup.head_object(Bucket=WORKBENCH_BUCKET, Key=SAMPLE_TOOL_ID) + assert False, "Object still exists in S3" + except botocore.exceptions.ClientError as e: + assert e.response["Error"]["Code"] == "404" + + +def test_delete_not_admin(s3_setup, lambda_context): + """Test unauthorized deletion of a tool.""" + # Create the event + event = { + "requestContext": {"authorizer": {"username": "regular-user"}}, + "pathParameters": {"toolId": SAMPLE_TOOL_ID}, + } + + # Import and test the function + from mcp_workbench.lambda_functions import delete + + # Use the actual function with moto S3 and patched is_admin + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "utilities.common_functions.get_username", return_value="regular-user" + ), patch("mcp_workbench.lambda_functions.api_wrapper", mock_api_wrapper): + response = delete(event, lambda_context) + + assert response["statusCode"] == 400 + body = json.loads(response["body"]) + # Handle both string and dict response formats + error_text = body if isinstance(body, str) else body.get("error", "") + assert "Only admin users can access tools" in error_text + + +def test_delete_not_found(s3_setup, lambda_context): + """Test deleting a non-existent tool.""" + # Create the event for a non-existent tool + event = { + "requestContext": {"authorizer": {"username": "test-admin"}}, + "pathParameters": {"toolId": "non_existent_tool.py"}, + } + + # Import and test the function + from mcp_workbench.lambda_functions import delete + + # Use the actual function with moto S3 + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "mcp_workbench.lambda_functions.is_admin", return_value=True + ), patch("mcp_workbench.lambda_functions.WORKBENCH_BUCKET", WORKBENCH_BUCKET): + response = delete(event, lambda_context) + + assert response["statusCode"] == 400 + body = json.loads(response["body"]) + # Handle both string and dict response formats + error_text = body if isinstance(body, str) else body.get("error", "") + assert "does not exist" in error_text + + +def test_delete_missing_tool_id(s3_setup, lambda_context): + """Test deleting without a toolId parameter.""" + # Create the event with missing pathParameters + event = { + "requestContext": {"authorizer": {"username": "test-admin"}}, + "pathParameters": {}, + } + + # Import and test the function + from mcp_workbench.lambda_functions import delete + + # Use the actual function with moto S3 + with patch("mcp_workbench.lambda_functions.s3_client", s3_setup), patch( + "mcp_workbench.lambda_functions.is_admin", return_value=True + ): + response = delete(event, lambda_context) + + assert response["statusCode"] == 400 + body = json.loads(response["body"]) + # Handle both string and dict response formats + error_text = body if isinstance(body, str) else body.get("error", "") + assert "Missing toolId parameter" in error_text diff --git a/test/lambda/test_repository_lambda.py b/test/lambda/test_repository_lambda.py index c1cdae0ab..3a2ed7206 100644 --- a/test/lambda/test_repository_lambda.py +++ b/test/lambda/test_repository_lambda.py @@ -198,7 +198,11 @@ def wrapper(event, context, *args, **kwargs): # Mock boto3 client function -def mock_boto3_client(service_name, region_name=None, config=None): +def mock_boto3_client(*args, **kwargs): + # Support both (service_name, region_name, config) and (service_name) + service_name = args[0] if args else kwargs.get("service_name") + if not service_name: + return MagicMock() # Fallback for any unexpected calls if service_name == "ssm": return mock_ssm elif service_name == "s3": @@ -247,8 +251,8 @@ def mock_boto3_client(service_name, region_name=None, config=None): patch("utilities.common_functions.get_cert_path", mock_common.get_cert_path).start() patch("utilities.auth.admin_only", mock_admin_only).start() -# Ensure mock_boto3_client is used for all boto3.client calls -patch("boto3.client", side_effect=mock_boto3_client).start() +# Note: boto3.client will be patched per-test to avoid global conflicts +# Global boto3.client patch removed to prevent interference with other test modules # Only now import the lambda functions to ensure they use our mocked dependencies from repository.lambda_functions import _ensure_document_ownership, _ensure_repository_access, presigned_url @@ -257,6 +261,13 @@ def mock_boto3_client(service_name, region_name=None, config=None): patch("utilities.vector_store.get_vector_store_client", mock_get_vector_store_client).start() +@pytest.fixture(autouse=True) +def mock_boto3_client_fixture(): + """Fixture to patch boto3.client for repository tests with proper isolation.""" + with patch("boto3.client", side_effect=mock_boto3_client): + yield + + @pytest.fixture def aws_credentials(): """Mocked AWS Credentials for moto.""" diff --git a/test/lambda/test_repository_state_machine_lambda.py b/test/lambda/test_repository_state_machine_lambda.py index bf1b39752..2321e57c3 100644 --- a/test/lambda/test_repository_state_machine_lambda.py +++ b/test/lambda/test_repository_state_machine_lambda.py @@ -59,8 +59,17 @@ def mock_boto3_client(service_name, region_name=None, config=None): return MagicMock() # Return a generic MagicMock for other services +# Note: boto3.client patch moved to fixture to avoid global conflicts + + +@pytest.fixture(autouse=True) +def mock_boto3_client_fixture(): + """Fixture to patch boto3.client for this test module with proper isolation.""" + with patch("boto3.client", side_effect=mock_boto3_client): + yield + + # Patch boto3.client to use our mock -patch("boto3.client", side_effect=mock_boto3_client).start() # Create mock modules for missing dependencies mock_vector_store_repo = MagicMock() diff --git a/test/lambda/test_s3_event_handler.py b/test/lambda/test_s3_event_handler.py new file mode 100644 index 000000000..327b545ed --- /dev/null +++ b/test/lambda/test_s3_event_handler.py @@ -0,0 +1,292 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for S3 event handler lambda function.""" + +import json +import os +import sys +from types import SimpleNamespace +from unittest.mock import patch + +import pytest +from botocore.exceptions import ClientError + +# Add the lambda directory to the Python path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../../")) + +mock_env = { + "AWS_ACCESS_KEY_ID": "testing", + "AWS_SECRET_ACCESS_KEY": "testing", + "AWS_SECURITY_TOKEN": "testing", + "AWS_SESSION_TOKEN": "testing", + "AWS_DEFAULT_REGION": "us-east-1", + "AWS_REGION": "us-east-1", + "DEPLOYMENT_PREFIX": "/test-deployment", + "API_NAME": "serve", + "ECS_CLUSTER_NAME": "test-deployment-serve", + "MCPWORKBENCH_SERVICE_NAME": "MCPWORKBENCH", +} + + +@pytest.fixture(autouse=True, scope="function") +def mock_common(): + """Ensure complete test isolation with fresh environment.""" + with patch.dict("os.environ", mock_env, clear=True): + yield + + +@pytest.fixture +def lambda_context(): + """Create a mock Lambda context.""" + return SimpleNamespace( + function_name="s3_event_handler", + function_version="$LATEST", + invoked_function_arn="arn:aws:lambda:us-east-1:123456789012:function:s3_event_handler", + memory_limit_in_mb=128, + aws_request_id="test-request-id", + log_group_name="/aws/lambda/s3_event_handler", + log_stream_name="2024/03/27/[$LATEST]test123", + ) + + +@pytest.fixture +def sample_s3_event(): + """Create a sample S3 event from EventBridge.""" + return { + "version": "0", + "id": "test-event-id", + "detail-type": "Object Created", + "source": "aws.s3", + "account": "123456789012", + "time": "2024-03-27T12:00:00Z", + "region": "us-east-1", + "detail": { + "version": "0", + "bucket": {"name": "test-deployment-dev-mcpworkbench"}, + "object": {"key": "test-tool.py", "size": 1024, "etag": "test-etag"}, + "eventName": "s3:ObjectCreated:Put", + "eventSource": "aws:s3", + }, + } + + +def test_handler_success(lambda_context, sample_s3_event): + """Test successful S3 event handling.""" + # Mock ECS client response + mock_ecs_response = { + "service": { + "serviceName": "MCPWORKBENCH", + "deployments": [ + { + "id": "arn:aws:ecs:us-east-1:123456789012:service/test-cluster/test-service/deployment-123", + "status": "PRIMARY", + } + ], + } + } + + # Import and test the function + from mcp_workbench.s3_event_handler import handler + + with patch("mcp_workbench.s3_event_handler.ecs_client") as mock_ecs_client: + mock_ecs_client.update_service.return_value = mock_ecs_response + + response = handler(sample_s3_event, lambda_context) + + # Verify the response + assert response["statusCode"] == 200 + body = json.loads(response["body"]) + assert body["message"] == "MCPWORKBENCH service redeployment triggered successfully" + assert body["bucket"] == "test-deployment-dev-mcpworkbench" + assert body["event"] == "s3:ObjectCreated:Put" + assert body["cluster"] == "test-deployment-serve" + assert body["service"] == "MCPWORKBENCH" + + # Verify ECS client was called correctly + mock_ecs_client.update_service.assert_called_once_with( + cluster="test-deployment-serve", service="MCPWORKBENCH", forceNewDeployment=True + ) + + +def test_handler_missing_bucket_name(lambda_context): + """Test handling of event with missing bucket name.""" + event = {"detail": {"eventName": "s3:ObjectCreated:Put"}} + + from mcp_workbench.s3_event_handler import handler + + response = handler(event, lambda_context) + + assert response["statusCode"] == 400 + assert "Missing bucket name" in response["body"] + + +def test_handler_ecs_service_not_found(lambda_context, sample_s3_event): + """Test handling of ECS service not found error.""" + from mcp_workbench.s3_event_handler import handler + + # Mock ECS client to raise ServiceNotFoundException + with patch("mcp_workbench.s3_event_handler.ecs_client") as mock_ecs_client: + mock_ecs_client.update_service.side_effect = ClientError( + error_response={"Error": {"Code": "ServiceNotFoundException", "Message": "Service not found"}}, + operation_name="UpdateService", + ) + + response = handler(sample_s3_event, lambda_context) + + assert response["statusCode"] == 500 + assert "Error" in response["body"] + + +def test_handler_ecs_cluster_not_found(lambda_context, sample_s3_event): + """Test handling of ECS cluster not found error.""" + from mcp_workbench.s3_event_handler import handler + + # Mock ECS client to raise ClusterNotFoundException + with patch("mcp_workbench.s3_event_handler.ecs_client") as mock_ecs_client: + mock_ecs_client.update_service.side_effect = ClientError( + error_response={"Error": {"Code": "ClusterNotFoundException", "Message": "Cluster not found"}}, + operation_name="UpdateService", + ) + + response = handler(sample_s3_event, lambda_context) + + assert response["statusCode"] == 500 + assert "Error" in response["body"] + + +def test_get_cluster_name_from_env(): + """Test getting cluster name from environment variable.""" + from mcp_workbench.s3_event_handler import get_cluster_name + + cluster_name = get_cluster_name() + assert cluster_name == "test-deployment-serve" + + +def test_get_cluster_name_fallback(): + """Test getting cluster name with fallback logic.""" + from mcp_workbench.s3_event_handler import get_cluster_name + + # Test without ECS_CLUSTER_NAME env var + with patch.dict(os.environ, {k: v for k, v in mock_env.items() if k != "ECS_CLUSTER_NAME"}): + with patch("mcp_workbench.s3_event_handler.ssm_client") as mock_ssm: + # Mock SSM parameter not found, should use fallback logic + mock_ssm.get_parameter.side_effect = ClientError( + error_response={"Error": {"Code": "ParameterNotFound"}}, operation_name="GetParameter" + ) + + cluster_name = get_cluster_name() + assert cluster_name == "test-deployment-serve" + + +def test_get_service_name_from_env(): + """Test getting service name from environment variable.""" + from mcp_workbench.s3_event_handler import get_service_name + + service_name = get_service_name() + assert service_name == "MCPWORKBENCH" + + +def test_get_service_name_fallback(): + """Test getting service name with fallback logic.""" + from mcp_workbench.s3_event_handler import get_service_name + + # Test without MCPWORKBENCH_SERVICE_NAME env var + with patch.dict(os.environ, {k: v for k, v in mock_env.items() if k != "MCPWORKBENCH_SERVICE_NAME"}): + service_name = get_service_name() + assert service_name == "MCPWORKBENCH" + + +def test_force_service_deployment_success(): + """Test successful ECS service deployment.""" + from mcp_workbench.s3_event_handler import force_service_deployment + + mock_response = {"service": {"serviceName": "MCPWORKBENCH", "deployments": [{"id": "deployment-123"}]}} + + with patch("mcp_workbench.s3_event_handler.ecs_client") as mock_ecs_client: + mock_ecs_client.update_service.return_value = mock_response + + response = force_service_deployment("test-cluster", "MCPWORKBENCH") + + assert response == mock_response + mock_ecs_client.update_service.assert_called_once_with( + cluster="test-cluster", service="MCPWORKBENCH", forceNewDeployment=True + ) + + +def test_force_service_deployment_service_not_found(): + """Test ECS service deployment with service not found.""" + from mcp_workbench.s3_event_handler import force_service_deployment + + with patch("mcp_workbench.s3_event_handler.ecs_client") as mock_ecs_client: + mock_ecs_client.update_service.side_effect = ClientError( + error_response={"Error": {"Code": "ServiceNotFoundException", "Message": "Service not found"}}, + operation_name="UpdateService", + ) + + with pytest.raises(ClientError): + force_service_deployment("test-cluster", "MCPWORKBENCH") + + +def test_validate_s3_event_valid(): + """Test validation of valid S3 event.""" + from mcp_workbench.s3_event_handler import validate_s3_event + + valid_event = {"source": "aws.s3", "detail-type": "Object Created", "detail": {"bucket": {"name": "test-bucket"}}} + + assert validate_s3_event(valid_event) is True + + +def test_validate_s3_event_invalid_source(): + """Test validation of S3 event with invalid source.""" + from mcp_workbench.s3_event_handler import validate_s3_event + + invalid_event = { + "source": "aws.ec2", + "detail-type": "Object Created", + "detail": {"bucket": {"name": "test-bucket"}}, + } + + assert validate_s3_event(invalid_event) is False + + +def test_validate_s3_event_invalid_detail_type(): + """Test validation of S3 event with invalid detail type.""" + from mcp_workbench.s3_event_handler import validate_s3_event + + invalid_event = { + "source": "aws.s3", + "detail-type": "Instance State Change", + "detail": {"bucket": {"name": "test-bucket"}}, + } + + assert validate_s3_event(invalid_event) is False + + +def test_validate_s3_event_missing_bucket(): + """Test validation of S3 event with missing bucket name.""" + from mcp_workbench.s3_event_handler import validate_s3_event + + invalid_event = {"source": "aws.s3", "detail-type": "Object Created", "detail": {}} + + assert validate_s3_event(invalid_event) is False + + +def test_validate_s3_event_debug_source(): + """Test validation of S3 event with debug source.""" + from mcp_workbench.s3_event_handler import validate_s3_event + + debug_event = {"source": "debug", "detail-type": "Object Created", "detail": {"bucket": {"name": "test-bucket"}}} + + assert validate_s3_event(debug_event) is True diff --git a/test/lambda/test_update_model_state_machine.py b/test/lambda/test_update_model_state_machine.py index b586db8a4..4b9bd63c4 100644 --- a/test/lambda/test_update_model_state_machine.py +++ b/test/lambda/test_update_model_state_machine.py @@ -137,7 +137,9 @@ # Mock boto3.client -def mock_boto3_client(service, **kwargs): +def mock_boto3_client(*args, **kwargs): + # Support both (service_name, region_name, config) and (service_name) + service = args[0] if args else kwargs.get("service_name", kwargs.get("service")) if service == "autoscaling": return mock_autoscaling elif service == "iam": @@ -162,6 +164,7 @@ def mock_boto3_client(service, **kwargs): return MagicMock() +# Note: This module needs global boto3.client patch for import-time dependencies patch("boto3.client", side_effect=mock_boto3_client).start() from models.domain_objects import ModelStatus