Skip to main content
Glama
hteek

Serverless MCP

by hteek
lambda.ts6.48 kB
import { Duration } from 'aws-cdk-lib'; import { ITable } from 'aws-cdk-lib/aws-dynamodb'; import { ArnPrincipal, Effect, PolicyStatement, Role, SessionTagsPrincipal, } from 'aws-cdk-lib/aws-iam'; import { Architecture, Function as Lambda, LambdaInsightsVersion, Runtime, Tracing, } from 'aws-cdk-lib/aws-lambda'; import { NodejsFunction, NodejsFunctionProps, OutputFormat, } from 'aws-cdk-lib/aws-lambda-nodejs'; import { RetentionDays } from 'aws-cdk-lib/aws-logs'; import { Construct } from 'constructs'; import { existsSync } from 'fs'; import { dirname, join, resolve } from 'path'; export interface BaseNodejsFunctionDynamodbProps { readonly table: ITable; readonly grantWriteData?: boolean; } export interface BaseNodejsFunctionProps { readonly dynamodb?: BaseNodejsFunctionDynamodbProps; readonly nodejsFunctionProps?: NodejsFunctionProps; } const decapitalize = (str?: string) => str ? str.charAt(0).toLowerCase() + str.slice(1) : ''; const addDynamoDb = ( fn: Lambda, role: Role, props?: BaseNodejsFunctionDynamodbProps ) => { const { table, grantWriteData } = props || {}; if (!table) { return; } fn.addEnvironment('TABLE', table.tableName); if (grantWriteData) { role.addToPolicy( new PolicyStatement({ effect: Effect.ALLOW, actions: [ 'dynamodb:BatchWriteItem', 'dynamodb:DeleteItem', 'dynamodb:PutItem', 'dynamodb:UpdateItem', ], resources: [table.tableArn], conditions: { 'ForAllValues:StringLike': { 'dynamodb:LeadingKeys': [ '${aws:PrincipalTag/TenantID}#*', 'public#*', ], }, }, }) ); } role.addToPolicy( new PolicyStatement({ effect: Effect.ALLOW, actions: [ 'dynamodb:BatchGetItem', 'dynamodb:ConditionCheckItem', 'dynamodb:DescribeTable', 'dynamodb:GetItem', 'dynamodb:GetRecords', 'dynamodb:GetShardIterator', 'dynamodb:Query', ], resources: [table.tableArn, `${table.tableArn}/index/*`], conditions: { 'ForAllValues:StringLike': { 'dynamodb:LeadingKeys': [ '${aws:PrincipalTag/TenantID}#*', 'public#*', ], }, }, }) ); }; export class BaseNodejsFunction extends NodejsFunction { readonly assumedRole: Role; constructor(scope: Construct, id: string, props?: BaseNodejsFunctionProps) { const { dynamodb, nodejsFunctionProps } = props ?? {}; super(scope, `${id}Function`, { // defaults architecture: Architecture.ARM_64, insightsVersion: LambdaInsightsVersion.VERSION_1_0_275_0, logRetention: RetentionDays.TWO_WEEKS, memorySize: 3008, // full capacity of a single cpu core for best single thread performance runtime: Runtime.NODEJS_22_X, timeout: Duration.minutes(2), tracing: Tracing.ACTIVE, // overrides ...nodejsFunctionProps, entry: resolve(findEntry(id, nodejsFunctionProps?.entry)), bundling: { // defaults banner: `import module from 'module'; if (typeof globalThis.require === "undefined") { globalThis.require = module.createRequire(import.meta.url); }`, format: OutputFormat.ESM, sourceMap: true, // custom environment ...nodejsFunctionProps?.bundling, externalModules: [], }, environment: { NODE_OPTIONS: '--enable-source-maps', POWERTOOLS_LOG_LEVEL: 'DEBUG', POWERTOOLS_LOGGER_LOG_EVENT: 'true', POWERTOOLS_LOGGER_SAMPLE_RATE: '0', POWERTOOLS_TRACE_ENABLED: 'true', POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', POWERTOOLS_TRACER_CAPTURE_HTTPS_REQUESTS: 'true', ...nodejsFunctionProps?.environment, }, }); // refers the Lambda function as an IAM Principal using its' ARN const arnPrincipal = new ArnPrincipal(this.role?.roleArn!).withConditions({ StringLike: { [`aws:RequestTag/TenantID`]: '*' }, }); // sets the allowed key for tagging sessions const sessionTagsPrincipal = new SessionTagsPrincipal(arnPrincipal); // granting the Lambda function the permission to assume this IAM Role this.assumedRole = new Role(scope, `${id}FunctionRole`, { assumedBy: sessionTagsPrincipal, }); this.addEnvironment('ASSUMED_ROLE_ARN', this.assumedRole.roleArn); addDynamoDb(this, this.assumedRole, dynamodb); } } const findEntry = (id: string, customEntry = 'lambda'): string => { const definingDirname = dirname(findDefiningFile()).replace(/^file:\/\//, ''); const handler = `${decapitalize(id)}Handler`; const tsHandlerFile = join(definingDirname, customEntry, `${handler}.ts`); if (existsSync(tsHandlerFile)) { return tsHandlerFile; } const jsHandlerFile = join(definingDirname, customEntry, `${handler}.js`); if (existsSync(jsHandlerFile)) { return jsHandlerFile; } const mjsHandlerFile = join(definingDirname, customEntry, `${handler}.mjs`); if (existsSync(mjsHandlerFile)) { return mjsHandlerFile; } throw new Error( `Cannot find handler file ${tsHandlerFile}, ${jsHandlerFile} or ${mjsHandlerFile}` ); }; const findDefiningFile = (): string => { let definingIndex; const sites = callsites(); for (const [index, site] of sites.entries()) { if (site.getFunctionName() === 'BaseNodejsFunction') { // The next site is the site where the BaseNodejsFunction was created definingIndex = index + 1; break; } } if (!definingIndex || !sites[definingIndex]) { throw new Error('Cannot find defining file.'); } return sites[definingIndex]?.getFileName() ?? ''; }; interface CallSite { getThis(): unknown; getTypeName(): string; getFunctionName(): string; getMethodName(): string; getFileName(): string; getLineNumber(): number; getColumnNumber(): number; getFunction(): unknown; getEvalOrigin(): string; isNative(): boolean; isToplevel(): boolean; isEval(): boolean; isConstructor(): boolean; } const callsites = (): CallSite[] => { const _prepareStackTrace = Error.prepareStackTrace; Error.prepareStackTrace = (_, stackTraces) => stackTraces; const stack = new Error().stack?.slice(1); Error.prepareStackTrace = _prepareStackTrace; return stack as unknown as CallSite[]; };

Latest Blog Posts

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/hteek/serverless-mcp'

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