Skip to content

Commit ee01a0e

Browse files
authored
fix: MCP build and deploy on AWS env (#691)
1 parent 931ad3f commit ee01a0e

16 files changed

Lines changed: 162 additions & 94 deletions

File tree

packages/backend/infra/stacks/api/stack.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,6 @@ export class ApiStack extends Stack {
7777
);
7878

7979
const healthCheckPath = '/lbcheck';
80-
81-
// AI Assistant configuration
82-
const aiEnabled = envSettings.aiConfig?.enabled ?? false;
8380
const mcpServerUrl = envSettings.aiConfig?.mcpServerUrl;
8481

8582
return new ApplicationMultipleTargetGroupsFargateService(
@@ -112,10 +109,7 @@ export class ApiStack extends Stack {
112109
csrfTrustedOrigins,
113110
mcpServerUrl,
114111
}),
115-
secrets: getBackendSecrets(this, {
116-
envSettings,
117-
includeAiSecrets: aiEnabled,
118-
}),
112+
secrets: getBackendSecrets(this, { envSettings }),
119113
},
120114
{
121115
containerName: 'xray-daemon',

packages/backend/infra/stacks/lib/names.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@ import { EnvironmentSettings } from '@sb/infra-core';
33
export function getBackendChamberServiceName(envSettings: EnvironmentSettings) {
44
return `env-${envSettings.projectEnvName}-backend`;
55
}
6+
7+
export function getMcpServerChamberServiceName(envSettings: EnvironmentSettings) {
8+
return `env-${envSettings.projectEnvName}-mcp-server`;
9+
}

packages/backend/infra/stacks/lib/secrets.ts

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,21 @@ import * as ecs from 'aws-cdk-lib/aws-ecs';
33
import * as sm from 'aws-cdk-lib/aws-secretsmanager';
44
import { EnvComponentsStack, MainDatabase } from '@sb/infra-shared';
55
import { Fn } from 'aws-cdk-lib';
6-
import { EnvironmentSettings } from '@sb/infra-core';
7-
8-
/**
9-
* Get the name for the AI secrets stored in AWS Secrets Manager.
10-
* Follows the project naming convention: env-{projectName}-{envStage}-ai-secrets
11-
*
12-
* To enable AI Assistant, create a secret in AWS Secrets Manager with this name
13-
* containing a JSON object with the following structure:
14-
* {
15-
* "OPENAI_API_KEY": "sk-..."
16-
* }
17-
*/
18-
export function getAiSecretsName(envSettings: EnvironmentSettings): string {
19-
return `env-${envSettings.projectName}-${envSettings.envStage}-ai-secrets`;
20-
}
6+
import type { EnvironmentSettings } from '@sb/infra-core';
217

228
interface GetBackendSecretsOptions {
239
envSettings: EnvironmentSettings;
24-
includeAiSecrets?: boolean;
2510
}
2611

2712
export function getBackendSecrets(
2813
scope: Construct,
29-
{ envSettings, includeAiSecrets = false }: GetBackendSecretsOptions,
14+
{ envSettings }: GetBackendSecretsOptions,
3015
) {
3116
const dbSecretArn = Fn.importValue(
3217
MainDatabase.getDatabaseSecretArnOutputExportName(envSettings),
3318
);
3419

35-
const baseSecrets = {
20+
return {
3621
DB_CONNECTION: ecs.Secret.fromSecretsManager(
3722
sm.Secret.fromSecretCompleteArn(scope, 'DbSecret', dbSecretArn),
3823
),
@@ -44,22 +29,4 @@ export function getBackendSecrets(
4429
),
4530
),
4631
};
47-
48-
// AI secrets are opt-in to avoid deployment failures when not configured
49-
if (includeAiSecrets) {
50-
const aiSecretsManager = sm.Secret.fromSecretNameV2(
51-
scope,
52-
'AiSecrets',
53-
getAiSecretsName(envSettings),
54-
);
55-
return {
56-
...baseSecrets,
57-
OPENAI_API_KEY: ecs.Secret.fromSecretsManager(
58-
aiSecretsManager,
59-
'OPENAI_API_KEY',
60-
),
61-
};
62-
}
63-
64-
return baseSecrets;
6532
}

packages/backend/infra/stacks/mcpServer/stack.ts

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import {
77
EnvConstructProps,
88
getHostedZone,
99
} from '@sb/infra-core';
10-
import { FargateServiceResources, MainECSCluster } from '@sb/infra-shared';
10+
import { FargateServiceResources, MainECSCluster, MainKmsKey } from '@sb/infra-shared';
1111

12+
import { getMcpServerChamberServiceName } from '../lib/names';
1213
import { getMcpServerServiceName } from './names';
1314

1415
export interface McpServerStackProps extends StackProps, EnvConstructProps {}
@@ -37,7 +38,6 @@ export class McpServerStack extends Stack {
3738
const resources = new FargateServiceResources(this, 'McpServerResources', props);
3839
this.fargateService = this.createFargateService(resources, props);
3940

40-
// Basic auto-scaling
4141
const scaling = this.fargateService.service.autoScaleTaskCount({
4242
maxCapacity: 3,
4343
});
@@ -52,14 +52,36 @@ export class McpServerStack extends Stack {
5252
props: McpServerStackProps,
5353
) {
5454
const { envSettings } = props;
55+
const stack = Stack.of(this);
5556

56-
// Create a basic task role for MCP server
5757
const taskRole = new iam.Role(this, 'McpServerTaskRole', {
5858
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
5959
});
6060

61-
// The MCP server needs to communicate with the backend API
62-
// The GraphQL endpoint is internal to the VPC
61+
const chamberServiceName = getMcpServerChamberServiceName(envSettings);
62+
taskRole.addToPolicy(
63+
new iam.PolicyStatement({
64+
actions: ['kms:Get*', 'kms:Describe*', 'kms:List*', 'kms:Decrypt'],
65+
resources: [
66+
Fn.importValue(MainKmsKey.getMainKmsOutputExportName(envSettings)),
67+
],
68+
}),
69+
);
70+
taskRole.addToPolicy(
71+
new iam.PolicyStatement({
72+
actions: ['ssm:DescribeParameters'],
73+
resources: ['*'],
74+
}),
75+
);
76+
taskRole.addToPolicy(
77+
new iam.PolicyStatement({
78+
actions: ['ssm:GetParameters*'],
79+
resources: [
80+
`arn:aws:ssm:${stack.region}:${stack.account}:parameter/${chamberServiceName}/*`,
81+
],
82+
}),
83+
);
84+
6385
const graphqlEndpoint = `https://${envSettings.domains.api}/api/graphql/`;
6486

6587
const httpsListener =
@@ -78,7 +100,6 @@ export class McpServerStack extends Stack {
78100

79101
const domainZone = getHostedZone(this, envSettings);
80102

81-
// MCP Server domain from environment configuration
82103
const mcpDomain = envSettings.domains.mcp;
83104

84105
return new ApplicationMultipleTargetGroupsFargateService(
@@ -89,7 +110,7 @@ export class McpServerStack extends Stack {
89110
serviceName: getMcpServerServiceName(props.envSettings),
90111
healthCheckGracePeriod: Duration.minutes(2),
91112
cluster: resources.mainCluster,
92-
cpu: 256, // MCP server is lightweight
113+
cpu: 256,
93114
memoryLimitMiB: 512,
94115
desiredCount: 1,
95116
taskRole,
@@ -103,7 +124,9 @@ export class McpServerStack extends Stack {
103124
environment: {
104125
GRAPHQL_ENDPOINT: graphqlEndpoint,
105126
MCP_LOG_LEVEL: 'info',
106-
MUTATION_MODE: 'explicit', // Only allow pre-defined mutations
127+
MUTATION_MODE: 'explicit',
128+
CHAMBER_SERVICE_NAME: chamberServiceName,
129+
CHAMBER_KMS_KEY_ALIAS: MainKmsKey.getKeyAlias(envSettings),
107130
},
108131
},
109132
],
@@ -118,10 +141,11 @@ export class McpServerStack extends Stack {
118141
targetGroups: [
119142
{
120143
protocol: ecs.Protocol.TCP,
121-
containerPort: 4000, // MCP server default port
122-
priority: 10, // Lower priority than API
144+
containerPort: 4000,
145+
targetProtocol: elb2.ApplicationProtocol.HTTP,
146+
priority: 10,
123147
hostHeader: mcpDomain,
124-
healthCheckPath: '/', // Basic health check - MCP server responds on root
148+
healthCheckPath: '/health',
125149
},
126150
],
127151
},

packages/infra/infra-core/src/lib/env-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ interface CertificatesConfig {
6767
* To enable the AI Assistant feature:
6868
* 1. Set SB_AI_ENABLED=true
6969
* 2. Set SB_MCP_SERVER_URL to your MCP server URL
70-
* 3. Create AWS Secrets Manager secret with OPENAI_API_KEY
70+
* 3. Add OPENAI_API_KEY via `pnpm saas backend secrets`
7171
*/
7272
export interface AiConfig {
7373
enabled: boolean;

packages/infra/infra-core/src/lib/patterns/applicationMultipleTargetGroupsFargateServiceBase.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,14 @@ export interface ApplicationTargetProps {
212212
*/
213213
readonly protocol?: Protocol;
214214

215+
/**
216+
* The protocol for connections from the load balancer to the target (HTTP or HTTPS).
217+
* Only needed for ports not in the default list (80, 443, 8080, 8000, 8008, 8443).
218+
*
219+
* @default - Inferred from port for known ports; HTTP for custom ports
220+
*/
221+
readonly targetProtocol?: ApplicationProtocol;
222+
215223
/**
216224
* Name of the listener the target group attached to.
217225
*
@@ -444,19 +452,24 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends Constru
444452
targets: ApplicationTargetProps[]
445453
): ApplicationTargetGroup {
446454
interface GroupedTarget {
447-
target: { protocol?: Protocol; containerPort: number };
455+
target: {
456+
protocol?: Protocol;
457+
containerPort: number;
458+
targetProtocol?: ApplicationProtocol;
459+
};
448460
hosts: ApplicationTargetProps[];
449461
healthCheckPath: string;
450462
}
451463

452464
const groupedTargets: { [id: string]: GroupedTarget } = {};
453465
targets?.forEach((targetProps) => {
454-
const key = `${targetProps.protocol}, ${targetProps.containerPort}`;
466+
const key = `${targetProps.protocol}, ${targetProps.containerPort}, ${targetProps.targetProtocol ?? 'default'}`;
455467
if (!(key in groupedTargets)) {
456468
groupedTargets[key] = {
457469
target: {
458470
protocol: targetProps.protocol,
459471
containerPort: targetProps.containerPort,
472+
targetProtocol: targetProps.targetProtocol,
460473
},
461474
hosts: [],
462475
healthCheckPath: targetProps.healthCheckPath,
@@ -471,6 +484,7 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends Constru
471484
{
472485
vpc: service.cluster.vpc,
473486
port: groupedTarget.target.containerPort,
487+
protocol: groupedTarget.target.targetProtocol,
474488
healthCheck: {
475489
path: groupedTarget.healthCheckPath,
476490
protocol: ELBProtocol.HTTP,

packages/infra/infra-core/src/lib/patterns/serviceCiConfig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface IServiceCiConfig {
1212

1313
export enum PnpmWorkspaceFilters {
1414
BACKEND = 'backend...',
15+
MCP_SERVER = 'mcp-server...',
1516
INFRA_SHARED = 'infra-shared...',
1617
DOCS = 'docs...',
1718
WEBAPP_EMAILS = 'webapp-emails...',

packages/infra/infra-shared/src/stacks/ci/ciMcpServer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export class McpServerCiConfig extends ServiceCiConfig {
7373

7474
private createBuildProject(props: McpServerCiConfigProps) {
7575
const preBuildCommands = [
76-
...this.getWorkspaceSetupCommands(PnpmWorkspaceFilters.INFRA_SHARED),
76+
...this.getWorkspaceSetupCommands(PnpmWorkspaceFilters.MCP_SERVER),
7777
];
7878

7979
const project = new codebuild.Project(this, 'McpServerBuildProject', {
@@ -86,7 +86,7 @@ export class McpServerCiConfig extends ServiceCiConfig {
8686
commands: preBuildCommands,
8787
},
8888
build: {
89-
commands: ['pnpm nx run mcp-server:build'],
89+
commands: ['pnpm saas mcp-server build'],
9090
},
9191
},
9292
}),
@@ -159,10 +159,10 @@ export class McpServerCiConfig extends ServiceCiConfig {
159159
install: this.getNodeInstallPhase(),
160160
pre_build: {
161161
commands: this.getWorkspaceSetupCommands(
162-
PnpmWorkspaceFilters.INFRA_SHARED,
162+
PnpmWorkspaceFilters.BACKEND,
163163
),
164164
},
165-
build: { commands: ['pnpm nx run mcp-server:deploy'] },
165+
build: { commands: ['pnpm saas mcp-server deploy'] },
166166
},
167167
cache: {
168168
paths: [...this.defaultCachePaths],
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { color } from '@oclif/color';
2+
3+
import { initConfig } from '../../config/init';
4+
import { assertDockerIsRunning, dockerHubLogin } from '../../lib/docker';
5+
import { runSecretsEditor } from '../../lib/secretsEditor';
6+
import { BaseCommand } from '../../baseCommand';
7+
8+
export default class McpServerSecrets extends BaseCommand<typeof McpServerSecrets> {
9+
static description =
10+
'Runs an ssm-editor helper tool to set runtime environment variables of the MCP server container. ' +
11+
'Uses Chamber to fetch and set variables in AWS SSM Parameter Store (e.g. OPENAI_API_KEY).';
12+
13+
static examples = [`$ <%= config.bin %> <%= command.id %>`];
14+
15+
async run(): Promise<void> {
16+
const { envStage, awsAccountId, awsRegion, rootPath } = await initConfig(
17+
this,
18+
{ requireAws: true },
19+
);
20+
await assertDockerIsRunning();
21+
await dockerHubLogin();
22+
23+
this.log(`Setting secrets in AWS SSM Parameter Store for:
24+
service: ${color.green('mcp-server')}
25+
envStage: ${color.green(envStage)}
26+
AWS account: ${color.green(awsAccountId)}
27+
AWS region: ${color.green(awsRegion)}
28+
`);
29+
30+
await runSecretsEditor({ serviceName: 'mcp-server', rootPath });
31+
}
32+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"root":["./src/basecommand.ts","./src/index.ts","./src/commands/build.ts","./src/commands/deploy.ts","./src/commands/down.ts","./src/commands/lint.ts","./src/commands/test.ts","./src/commands/up.ts","./src/commands/aws/get-env.ts","./src/commands/aws/login.ts","./src/commands/aws/set-env.ts","./src/commands/aws/set-var.ts","./src/commands/backend/black.ts","./src/commands/backend/build-docs.ts","./src/commands/backend/build.ts","./src/commands/backend/down.ts","./src/commands/backend/makemigrations.ts","./src/commands/backend/migrate.ts","./src/commands/backend/remote-shell.ts","./src/commands/backend/ruff.ts","./src/commands/backend/secrets.ts","./src/commands/backend/shell.ts","./src/commands/backend/test.ts","./src/commands/backend/up.ts","./src/commands/backend/deploy/api.ts","./src/commands/backend/deploy/celery.ts","./src/commands/backend/deploy/demo-cleanup.ts","./src/commands/backend/deploy/mcp-server.ts","./src/commands/backend/deploy/migrations.ts","./src/commands/backend/stripe/sync.ts","./src/commands/ci/get-artifacts-bucket.ts","./src/commands/db/shell.ts","./src/commands/docs/build.ts","./src/commands/docs/deploy.ts","./src/commands/docs/up.ts","./src/commands/emails/build.ts","./src/commands/emails/secrets.ts","./src/commands/emails/test.ts","./src/commands/infra/bootstrap.ts","./src/commands/infra/deploy.ts","./src/commands/mcp-server/build.ts","./src/commands/mcp-server/deploy.ts","./src/commands/render/deploy.ts","./src/commands/render/setup.ts","./src/commands/render/status.ts","./src/commands/vps/deploy.ts","./src/commands/vps/setup.ts","./src/commands/vps/ssh.ts","./src/commands/vps/status.ts","./src/commands/webapp/build.ts","./src/commands/webapp/deploy.ts","./src/commands/webapp/lint.ts","./src/commands/webapp/secrets.ts","./src/commands/webapp/storybook.ts","./src/commands/webapp/test.ts","./src/commands/webapp/type-check.ts","./src/commands/webapp/up.ts","./src/commands/webapp/graphql/download-schema.ts","./src/commands/workers/black.ts","./src/commands/workers/build.ts","./src/commands/workers/deploy.ts","./src/commands/workers/lint.ts","./src/commands/workers/secrets.ts","./src/commands/workers/shell.ts","./src/commands/workers/test.ts","./src/commands/workers/invoke/local.ts","./src/config/aws.ts","./src/config/env.ts","./src/config/init.ts","./src/config/platform.ts","./src/config/storage.ts","./src/config/telemetry.ts","./src/hooks/init/instrumentation.ts","./src/lib/awsvault.ts","./src/lib/chamber.ts","./src/lib/docker.ts","./src/lib/preflight.ts","./src/lib/prompts.ts","./src/lib/renderapi.ts","./src/lib/runcommand.ts","./src/lib/secretseditor.ts","./src/lib/ui/banner.ts","./src/lib/ui/healthdashboard.ts","./src/lib/ui/index.ts","./src/lib/ui/keyboard.ts","./src/lib/ui/renderer.ts","./src/lib/ui/spinner.ts"],"version":"5.9.3"}
1+
{"root":["./src/basecommand.ts","./src/index.ts","./src/commands/build.ts","./src/commands/deploy.ts","./src/commands/down.ts","./src/commands/lint.ts","./src/commands/test.ts","./src/commands/up.ts","./src/commands/aws/get-env.ts","./src/commands/aws/login.ts","./src/commands/aws/set-env.ts","./src/commands/aws/set-var.ts","./src/commands/backend/black.ts","./src/commands/backend/build-docs.ts","./src/commands/backend/build.ts","./src/commands/backend/down.ts","./src/commands/backend/makemigrations.ts","./src/commands/backend/migrate.ts","./src/commands/backend/remote-shell.ts","./src/commands/backend/ruff.ts","./src/commands/backend/secrets.ts","./src/commands/backend/shell.ts","./src/commands/backend/test.ts","./src/commands/backend/up.ts","./src/commands/backend/deploy/api.ts","./src/commands/backend/deploy/celery.ts","./src/commands/backend/deploy/demo-cleanup.ts","./src/commands/backend/deploy/mcp-server.ts","./src/commands/backend/deploy/migrations.ts","./src/commands/backend/stripe/sync.ts","./src/commands/ci/get-artifacts-bucket.ts","./src/commands/db/shell.ts","./src/commands/docs/build.ts","./src/commands/docs/deploy.ts","./src/commands/docs/up.ts","./src/commands/emails/build.ts","./src/commands/emails/secrets.ts","./src/commands/emails/test.ts","./src/commands/infra/bootstrap.ts","./src/commands/infra/deploy.ts","./src/commands/mcp-server/build.ts","./src/commands/mcp-server/deploy.ts","./src/commands/mcp-server/secrets.ts","./src/commands/render/deploy.ts","./src/commands/render/setup.ts","./src/commands/render/status.ts","./src/commands/vps/deploy.ts","./src/commands/vps/setup.ts","./src/commands/vps/ssh.ts","./src/commands/vps/status.ts","./src/commands/webapp/build.ts","./src/commands/webapp/deploy.ts","./src/commands/webapp/lint.ts","./src/commands/webapp/secrets.ts","./src/commands/webapp/storybook.ts","./src/commands/webapp/test.ts","./src/commands/webapp/type-check.ts","./src/commands/webapp/up.ts","./src/commands/webapp/graphql/download-schema.ts","./src/commands/workers/black.ts","./src/commands/workers/build.ts","./src/commands/workers/deploy.ts","./src/commands/workers/lint.ts","./src/commands/workers/secrets.ts","./src/commands/workers/shell.ts","./src/commands/workers/test.ts","./src/commands/workers/invoke/local.ts","./src/config/aws.ts","./src/config/env.ts","./src/config/init.ts","./src/config/platform.ts","./src/config/storage.ts","./src/config/telemetry.ts","./src/hooks/init/instrumentation.ts","./src/lib/awsvault.ts","./src/lib/chamber.ts","./src/lib/docker.ts","./src/lib/preflight.ts","./src/lib/prompts.ts","./src/lib/renderapi.ts","./src/lib/runcommand.ts","./src/lib/secretseditor.ts","./src/lib/ui/banner.ts","./src/lib/ui/healthdashboard.ts","./src/lib/ui/index.ts","./src/lib/ui/keyboard.ts","./src/lib/ui/renderer.ts","./src/lib/ui/spinner.ts"],"version":"5.9.3"}

0 commit comments

Comments
 (0)