Skip to main content
Glama

TimeLooker MCP Server

timelooker_stack.py13.7 kB
from aws_cdk import ( Stack, aws_ec2 as ec2, aws_rds as rds, aws_s3 as s3, aws_ses as ses, aws_secretsmanager as secretsmanager, aws_iam as iam, CfnOutput, RemovalPolicy, Duration ) from constructs import Construct class TimeLookerStack(Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) # VPC with public subnets only (no NAT Gateway needed) self.vpc = ec2.Vpc( self, "TimeLookerVPC", max_azs=2, nat_gateways=0, # No NAT Gateway to save costs subnet_configuration=[ ec2.SubnetConfiguration( subnet_type=ec2.SubnetType.PUBLIC, name="PublicSubnet", cidr_mask=24 ) ] ) # Security group for RDS self.db_security_group = ec2.SecurityGroup( self, "TimeLookerDBSecurityGroup", vpc=self.vpc, description="Security group for TimeLooker RDS database", allow_all_outbound=False ) # Allow PostgreSQL access from Lambda (and development) self.db_security_group.add_ingress_rule( peer=ec2.Peer.any_ipv4(), # Public access with security group connection=ec2.Port.tcp(5432), description="PostgreSQL access" ) # RDS PostgreSQL Database self.database = rds.DatabaseInstance( self, "TimeLookerDatabase", engine=rds.DatabaseInstanceEngine.postgres( version=rds.PostgresEngineVersion.VER_15_13 ), instance_type=ec2.InstanceType.of( ec2.InstanceClass.BURSTABLE3, ec2.InstanceSize.MICRO # db.t3.micro - cheapest option ), database_name="timelooker", credentials=rds.Credentials.from_generated_secret( "timelookeradmin", # No hyphens allowed in RDS username secret_name="timelooker/database/credentials" ), vpc=self.vpc, vpc_subnets=ec2.SubnetSelection( subnet_type=ec2.SubnetType.PUBLIC ), security_groups=[self.db_security_group], allocated_storage=20, # 20 GB minimum storage_type=rds.StorageType.GP2, backup_retention=Duration.days(1), # Minimal backup for dev deletion_protection=False, # Allow deletion for dev environment publicly_accessible=True, # Public access via security group removal_policy=RemovalPolicy.DESTROY # Allow CDK to delete ) # S3 Bucket for email templates self.email_templates_bucket = s3.Bucket( self, "TimeLookerEmailTemplates", bucket_name=f"timelooker-email-templates-{self.account}-{self.region}", versioned=False, public_read_access=False, removal_policy=RemovalPolicy.DESTROY, auto_delete_objects=True # Clean up on stack deletion ) # Note: Email template will be uploaded separately after deployment # The template is defined in _get_default_email_template() method below # Secrets Manager for API keys self.openai_secret = secretsmanager.Secret( self, "TimeLookerOpenAISecret", secret_name="timelooker/openai/api-key", description="OpenAI API key for TimeLooker search comparison", generate_secret_string=secretsmanager.SecretStringGenerator( secret_string_template='{"api_key": ""}', generate_string_key="api_key", exclude_characters=' "\\/@' ) ) self.anthropic_secret = secretsmanager.Secret( self, "TimeLookerAnthropicSecret", secret_name="timelooker/anthropic/api-key", description="Anthropic API key for TimeLooker alternative search engine", generate_secret_string=secretsmanager.SecretStringGenerator( secret_string_template='{"api_key": ""}', generate_string_key="api_key", exclude_characters=' "\\/@' ) ) self.x402_secret = secretsmanager.Secret( self, "TimeLookerX402Secret", secret_name="timelooker/x402/private-key", description="X402 private key for payment processing", generate_secret_string=secretsmanager.SecretStringGenerator( secret_string_template='{"private_key": ""}', generate_string_key="private_key", exclude_characters=' "\\/@' ) ) # IAM role for Lambda functions self.lambda_execution_role = iam.Role( self, "TimeLookerLambdaExecutionRole", assumed_by=iam.CompositePrincipal( iam.ServicePrincipal("lambda.amazonaws.com"), iam.ServicePrincipal("scheduler.amazonaws.com") # Allow EventBridge Scheduler ), managed_policies=[ iam.ManagedPolicy.from_aws_managed_policy_name( "service-role/AWSLambdaBasicExecutionRole" ) ] ) # Add permissions for Lambda to access resources self.lambda_execution_role.add_to_policy( iam.PolicyStatement( effect=iam.Effect.ALLOW, actions=[ "secretsmanager:GetSecretValue" ], resources=[ self.openai_secret.secret_arn, self.anthropic_secret.secret_arn, self.x402_secret.secret_arn, self.database.secret.secret_arn ] ) ) self.lambda_execution_role.add_to_policy( iam.PolicyStatement( effect=iam.Effect.ALLOW, actions=[ "s3:GetObject" ], resources=[ f"{self.email_templates_bucket.bucket_arn}/*" ] ) ) self.lambda_execution_role.add_to_policy( iam.PolicyStatement( effect=iam.Effect.ALLOW, actions=[ "ses:SendEmail", "ses:SendRawEmail" ], resources=["*"] # SES requires wildcard for email sending ) ) # SES Email Identity (will need manual verification) # Note: This creates the identity but email verification must be done manually self.ses_email_identity = ses.CfnEmailIdentity( self, "TimeLookerSESIdentity", email_identity="fortnightlydevs@gmail.com" # Replace with your domain ) # IAM role for the main application to manage Lambda functions self.app_execution_role = iam.Role( self, "TimeLookerAppExecutionRole", assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"), managed_policies=[ iam.ManagedPolicy.from_aws_managed_policy_name( "service-role/AWSLambdaBasicExecutionRole" ) ] ) # Add permissions for the app to manage Lambda functions and EventBridge self.app_execution_role.add_to_policy( iam.PolicyStatement( effect=iam.Effect.ALLOW, actions=[ "lambda:CreateFunction", "lambda:DeleteFunction", "lambda:UpdateFunctionCode", "lambda:UpdateFunctionConfiguration", "lambda:GetFunction", "lambda:InvokeFunction", "lambda:AddPermission", "lambda:RemovePermission" ], resources=["*"] ) ) self.app_execution_role.add_to_policy( iam.PolicyStatement( effect=iam.Effect.ALLOW, actions=[ "events:PutRule", "events:DeleteRule", "events:PutTargets", "events:RemoveTargets", "events:DescribeRule", "scheduler:CreateSchedule", "scheduler:DeleteSchedule", "scheduler:UpdateSchedule", "scheduler:GetSchedule" ], resources=["*"] ) ) self.app_execution_role.add_to_policy( iam.PolicyStatement( effect=iam.Effect.ALLOW, actions=[ "iam:PassRole" ], resources=[self.lambda_execution_role.role_arn] ) ) # Outputs for use in the application CfnOutput( self, "DatabaseEndpoint", value=self.database.instance_endpoint.hostname, description="RDS PostgreSQL endpoint" ) CfnOutput( self, "DatabasePort", value=str(self.database.instance_endpoint.port), description="RDS PostgreSQL port" ) CfnOutput( self, "DatabaseName", value="timelooker", description="Database name" ) CfnOutput( self, "DatabaseSecretArn", value=self.database.secret.secret_arn, description="ARN of the database credentials secret" ) CfnOutput( self, "EmailTemplatesBucket", value=self.email_templates_bucket.bucket_name, description="S3 bucket for email templates" ) CfnOutput( self, "OpenAISecretArn", value=self.openai_secret.secret_arn, description="ARN of the OpenAI API key secret" ) CfnOutput( self, "AnthropicSecretArn", value=self.anthropic_secret.secret_arn, description="ARN of the Anthropic API key secret" ) CfnOutput( self, "X402SecretArn", value=self.x402_secret.secret_arn, description="ARN of the X402 private key secret" ) CfnOutput( self, "LambdaExecutionRoleArn", value=self.lambda_execution_role.role_arn, description="ARN of the Lambda execution role" ) CfnOutput( self, "AppExecutionRoleArn", value=self.app_execution_role.role_arn, description="ARN of the application execution role" ) CfnOutput( self, "SESEmailIdentity", value=self.ses_email_identity.email_identity, description="SES email identity (requires manual verification)" ) def _get_default_email_template(self) -> str: """Default HTML email template for notifications.""" return """<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>TimeLooker Notification</title> <style> body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; } .header { background: #2c3e50; color: white; padding: 20px; text-align: center; border-radius: 5px 5px 0 0; } .content { background: #f8f9fa; padding: 20px; border: 1px solid #dee2e6; } .footer { background: #6c757d; color: white; padding: 15px; text-align: center; border-radius: 0 0 5px 5px; font-size: 0.9em; } .item { background: white; margin: 10px 0; padding: 15px; border-left: 4px solid #007bff; border-radius: 3px; } .item h3 { margin: 0 0 10px 0; color: #007bff; } .item .meta { color: #6c757d; font-size: 0.9em; margin: 5px 0; } .item .description { margin: 10px 0; } .item .url { word-break: break-all; } </style> </head> <body> <div class="header"> <h1>🔍 TimeLooker Notification</h1> <p>New items found for your monitoring task</p> </div> <div class="content"> <h2>Task: {{task_description}}</h2> <p><strong>Time:</strong> {{timestamp}}</p> <p><strong>New items found:</strong> {{new_items_count}}</p> <h3>New Items:</h3> {{#new_items}} <div class="item"> <h3>{{name}}</h3> <div class="meta"> <strong>Source:</strong> {{source}} | <strong>Location:</strong> {{location}} </div> {{#description}} <div class="description">{{description}}</div> {{/description}} {{#url}} <div class="meta"> <strong>URL:</strong> <a href="{{url}}" target="_blank">{{url}}</a> </div> {{/url}} {{#additional_info}} <div class="meta"> <strong>Additional Info:</strong> {{additional_info}} </div> {{/additional_info}} </div> {{/new_items}} </div> <div class="footer"> <p>This is an automated message from TimeLooker MCP Server</p> <p>Powered by AWS Lambda, RDS, and SES</p> </div> </body> </html>"""

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/fortnightly-devs/mcp-x402-task-scheduler'

If you have feedback or need assistance with the MCP directory API, please join our Discord server