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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,13 @@ def customize_components(
del component.parameters["properties"][key]


def main():
def _create_mcp_server(stateless_http: bool = False) -> FastMCP:
"""Create and configure the FastMCP server instance."""
from solace_event_portal_designer_mcp import __version__

logger.info(f"Starting Solace Event Portal Designer MCP Server v{__version__}")

# Create an HTTP client for your API
base_url = os.getenv("SOLACE_API_BASE_URL", default="https://api.solace.cloud")
token = os.getenv("SOLACE_API_TOKEN")
headers_for_tracability={
headers_for_tracability = {
"User-Agent": f"solace/event-portal-designer-mcp/{__version__}",
"x-issuer": f"solace/event-portal-designer-mcp/{__version__}"
}
Expand All @@ -72,8 +70,6 @@ def main():
client.headers.update(headers_for_tracability)
logger.debug("HTTP client configured with authentication and custom headers")


# Load your OpenAPI spec
spec_path = os.path.join(os.path.dirname(__file__), "data", "ep-designer.json")
logger.debug(f"Loading OpenAPI specification from {spec_path}")
try:
Expand All @@ -82,28 +78,26 @@ def main():
logger.debug("OpenAPI specification loaded successfully")
except FileNotFoundError:
logger.error(f"OpenAPI spec file not found at {spec_path}")
sys.exit(1)
raise
except json.JSONDecodeError as e:
logger.error(f"Invalid JSON in OpenAPI spec: {e}")
sys.exit(1)
raise

# There are some cyclical references in the OpenAPI spec that need to be resolved before passing it to FastMCP
# Manual patch for circular references:
# The "InvalidStateReference" schema has properties "inboundInvalidStateReferences" and "outboundInvalidStateReferences"
# which reference arrays of "InvalidStateReference" objects, creating a circular reference in the OpenAPI spec.
# This causes issues with tools like FastMCP that cannot process such cycles.
# To break the cycle, we replace the "items" schema for these properties with a generic object.
# This loses schema detail for these properties, but is necessary for compatibility.
# Manual patch for circular references in the OpenAPI spec:
# The "InvalidStateReference" schema has properties "inboundInvalidStateReferences" and
# "outboundInvalidStateReferences" which reference arrays of "InvalidStateReference" objects,
# creating a circular reference. FastMCP cannot process such cycles, so we replace the "items"
# schema for these properties with a generic object. This loses schema detail but is necessary.
logger.info("Patching circular references in OpenAPI specification")
openapi_spec["components"]["schemas"]["InvalidStateReference"]["properties"]["inboundInvalidStateReferences"]["items"] = {"type": "object"}
openapi_spec["components"]["schemas"]["InvalidStateReference"]["properties"]["outboundInvalidStateReferences"]["items"] = {"type": "object"}

# Create the MCP server
logger.info("Creating MCP server from OpenAPI specification")
mcp = FastMCP.from_openapi(
return FastMCP.from_openapi(
openapi_spec=openapi_spec,
client=client,
name="EP Designer API",
stateless_http=stateless_http,
route_maps=[
RouteMap(pattern=r"^/api/v2/architecture/applicationDomains(/\{id\})?$", mcp_type=MCPType.TOOL),
RouteMap(pattern=r"^/api/v2/architecture/applications(/\{id\})?$", mcp_type=MCPType.TOOL),
Expand All @@ -118,7 +112,20 @@ def main():
mcp_component_fn=customize_components,
)


def create_mcp_http_app():
"""Create and return the FastMCP HTTP app for use in HTTP contexts like Lambda."""
mcp = _create_mcp_server(stateless_http=True)
return mcp.http_app(path="/mcp")


def main():
from solace_event_portal_designer_mcp import __version__

logger.info(f"Starting Solace Event Portal Designer MCP Server v{__version__}")

try:
mcp = _create_mcp_server()
logger.info("Starting MCP server...")
mcp.run()
except KeyboardInterrupt:
Expand Down
50 changes: 50 additions & 0 deletions terraform/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Terraform
.terraform/
.terraform.lock.hcl
terraform.tfstate
terraform.tfstate.backup
*.tfstate
*.tfstate.*

# Sensitive files
terraform.tfvars
*.tfvars
*.auto.tfvars

# Lambda build artifacts
package/
lambda_function.zip
*.zip

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# IDE
.vscode/
.idea/
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db
30 changes: 30 additions & 0 deletions terraform/lambda_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import json
import logging
from mangum import Mangum
from solace_event_portal_designer_mcp.server import create_mcp_http_app

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


def handler(event, context):
"""Lambda handler that calls the EP MCP module."""
logger.info(f"Request: {event.get('rawPath')}")

try:
app = create_mcp_http_app()
asgi_handler = Mangum(app, lifespan="on")
return asgi_handler(event, context)
except Exception as e:
logger.error(f"Handler error: {e}", exc_info=True)
return {
"statusCode": 500,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({
"jsonrpc": "2.0",
"error": {
"code": -32603,
"message": f"Internal error: {str(e)}"
}
})
}
164 changes: 164 additions & 0 deletions terraform/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
null = {
source = "hashicorp/null"
version = "~> 3.0"
}
}
}

provider "aws" {
region = var.aws_region
}

variable "aws_region" {
type = string
default = "us-east-1"
}

variable "function_name" {
type = string
default = "solace-event-portal-mcp"
}

variable "solace_api_token" {
type = string
sensitive = true
}

variable "solace_api_base_url" {
type = string
default = "https://api.solace.cloud"
}

variable "python_version" {
type = string
default = "python3.12"
}

resource "null_resource" "build_lambda_package" {
triggers = {
requirements = filemd5("${path.module}/../solace-event-portal-designer-mcp/requirements.txt")
source_code = sha256(join("", [for f in fileset("${path.module}/../solace-event-portal-designer-mcp/src", "**/*.py") : filesha256("${path.module}/../solace-event-portal-designer-mcp/src/${f}")]))
}

provisioner "local-exec" {
command = <<-EOT
set -e
rm -rf ${path.module}/package
mkdir -p ${path.module}/package

pip3 install \
--target ${path.module}/package \
--platform manylinux2014_x86_64 \
--implementation cp \
--python-version 3.12 \
--only-binary=:all: \
--upgrade \
-r ${path.module}/../solace-event-portal-designer-mcp/requirements.txt

pip3 install \
--target ${path.module}/package \
--platform manylinux2014_x86_64 \
--implementation cp \
--python-version 3.12 \
--only-binary=:all: \
--upgrade \
mangum

cp -r ${path.module}/../solace-event-portal-designer-mcp/src/* ${path.module}/package/
cp ${path.module}/lambda_handler.py ${path.module}/package/

if [ -d "${path.module}/../solace-event-portal-designer-mcp/src/solace_event_portal_designer_mcp/data" ]; then
mkdir -p ${path.module}/package/solace_event_portal_designer_mcp/data
cp -r ${path.module}/../solace-event-portal-designer-mcp/src/solace_event_portal_designer_mcp/data/* \
${path.module}/package/solace_event_portal_designer_mcp/data/
fi

find ${path.module}/package -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
find ${path.module}/package -type f -name "*.pyc" -delete 2>/dev/null || true
EOT
}
}

data "archive_file" "lambda_package" {
type = "zip"
source_dir = "${path.module}/package"
output_path = "${path.module}/lambda_function.zip"

depends_on = [null_resource.build_lambda_package]
}

resource "aws_iam_role" "lambda_role" {
name = "${var.function_name}-role"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}]
})
}

resource "aws_iam_role_policy_attachment" "lambda_basic" {
role = aws_iam_role.lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_cloudwatch_log_group" "lambda_logs" {
name = "/aws/lambda/${var.function_name}"
retention_in_days = 7
}

resource "aws_lambda_function" "mcp_server" {
filename = data.archive_file.lambda_package.output_path
function_name = var.function_name
role = aws_iam_role.lambda_role.arn
handler = "lambda_handler.handler"
source_code_hash = data.archive_file.lambda_package.output_base64sha256
runtime = var.python_version
timeout = 300
memory_size = 512

environment {
variables = {
SOLACE_API_TOKEN = var.solace_api_token
SOLACE_API_BASE_URL = var.solace_api_base_url
}
}

depends_on = [
aws_cloudwatch_log_group.lambda_logs,
aws_iam_role_policy_attachment.lambda_basic
]
}

resource "aws_lambda_function_url" "mcp_server_url" {
function_name = aws_lambda_function.mcp_server.function_name
authorization_type = "NONE"
}

output "lambda_function_name" {
value = aws_lambda_function.mcp_server.function_name
}

output "lambda_function_arn" {
value = aws_lambda_function.mcp_server.arn
}

output "lambda_function_url" {
value = aws_lambda_function_url.mcp_server_url.function_url
}

output "lambda_role_arn" {
value = aws_iam_role.lambda_role.arn
}