Skip to content

Commit 1baab37

Browse files
committed
feat: enable gateway commands, UI updates, deploy pipeline, credential validation (#382)
1 parent 7f2efe9 commit 1baab37

25 files changed

Lines changed: 553 additions & 133 deletions

File tree

src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { AgentCoreStack } from '../lib/cdk-stack';
4848
import { ConfigIO, type AwsDeploymentTarget } from '@aws/agentcore-cdk';
4949
import { App, type Environment } from 'aws-cdk-lib';
5050
import * as path from 'path';
51+
import * as fs from 'fs';
5152
5253
function toEnvironment(target: AwsDeploymentTarget): Environment {
5354
return {
@@ -72,6 +73,17 @@ async function main() {
7273
const spec = await configIO.readProjectSpec();
7374
const targets = await configIO.readAWSDeploymentTargets();
7475
76+
// Read MCP configuration if it exists
77+
let mcpSpec;
78+
let mcpDeployedState;
79+
try {
80+
mcpSpec = await configIO.readMcpSpec();
81+
const deployedState = JSON.parse(fs.readFileSync(path.join(configRoot, '.cli', 'deployed-state.json'), 'utf8'));
82+
mcpDeployedState = deployedState?.mcp;
83+
} catch {
84+
// MCP config is optional
85+
}
86+
7587
if (targets.length === 0) {
7688
throw new Error('No deployment targets configured. Please define targets in agentcore/aws-targets.json');
7789
}
@@ -84,6 +96,8 @@ async function main() {
8496
8597
new AgentCoreStack(app, stackName, {
8698
spec,
99+
mcpSpec,
100+
mcpDeployedState,
87101
env,
88102
description: \`AgentCore stack for \${spec.name} deployed to \${target.name} (\${target.region})\`,
89103
tags: {
@@ -222,7 +236,13 @@ exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/jest.config.js should
222236
`;
223237
224238
exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/lib/cdk-stack.ts should match snapshot 1`] = `
225-
"import { AgentCoreApplication, type AgentCoreProjectSpec } from '@aws/agentcore-cdk';
239+
"import {
240+
AgentCoreApplication,
241+
AgentCoreMcp,
242+
type AgentCoreProjectSpec,
243+
type McpSpec,
244+
type McpDeployedState,
245+
} from '@aws/agentcore-cdk';
226246
import { CfnOutput, Stack, type StackProps } from 'aws-cdk-lib';
227247
import { Construct } from 'constructs';
228248
@@ -231,6 +251,14 @@ export interface AgentCoreStackProps extends StackProps {
231251
* The AgentCore project specification containing agents, memories, and credentials.
232252
*/
233253
spec: AgentCoreProjectSpec;
254+
/**
255+
* The MCP specification containing gateways and servers.
256+
*/
257+
mcpSpec?: McpSpec;
258+
/**
259+
* The MCP deployed state.
260+
*/
261+
mcpDeployedState?: McpDeployedState;
234262
}
235263
236264
/**
@@ -246,13 +274,22 @@ export class AgentCoreStack extends Stack {
246274
constructor(scope: Construct, id: string, props: AgentCoreStackProps) {
247275
super(scope, id, props);
248276
249-
const { spec } = props;
277+
const { spec, mcpSpec, mcpDeployedState } = props;
250278
251279
// Create AgentCoreApplication with all agents
252280
this.application = new AgentCoreApplication(this, 'Application', {
253281
spec,
254282
});
255283
284+
// Create AgentCoreMcp if there are gateways configured
285+
if (mcpSpec?.agentCoreGateways && mcpSpec.agentCoreGateways.length > 0) {
286+
new AgentCoreMcp(this, 'Mcp', {
287+
spec: mcpSpec,
288+
deployedState: mcpDeployedState,
289+
application: this.application,
290+
});
291+
}
292+
256293
// Stack-level output
257294
new CfnOutput(this, 'StackNameOutput', {
258295
description: 'Name of the CloudFormation Stack',

src/assets/cdk/bin/cdk.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { AgentCoreStack } from '../lib/cdk-stack';
33
import { ConfigIO, type AwsDeploymentTarget } from '@aws/agentcore-cdk';
44
import { App, type Environment } from 'aws-cdk-lib';
55
import * as path from 'path';
6+
import * as fs from 'fs';
67

78
function toEnvironment(target: AwsDeploymentTarget): Environment {
89
return {
@@ -27,6 +28,17 @@ async function main() {
2728
const spec = await configIO.readProjectSpec();
2829
const targets = await configIO.readAWSDeploymentTargets();
2930

31+
// Read MCP configuration if it exists
32+
let mcpSpec;
33+
let mcpDeployedState;
34+
try {
35+
mcpSpec = await configIO.readMcpSpec();
36+
const deployedState = JSON.parse(fs.readFileSync(path.join(configRoot, '.cli', 'deployed-state.json'), 'utf8'));
37+
mcpDeployedState = deployedState?.mcp;
38+
} catch {
39+
// MCP config is optional
40+
}
41+
3042
if (targets.length === 0) {
3143
throw new Error('No deployment targets configured. Please define targets in agentcore/aws-targets.json');
3244
}
@@ -39,6 +51,8 @@ async function main() {
3951

4052
new AgentCoreStack(app, stackName, {
4153
spec,
54+
mcpSpec,
55+
mcpDeployedState,
4256
env,
4357
description: `AgentCore stack for ${spec.name} deployed to ${target.name} (${target.region})`,
4458
tags: {

src/assets/cdk/lib/cdk-stack.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { AgentCoreApplication, type AgentCoreProjectSpec } from '@aws/agentcore-cdk';
1+
import {
2+
AgentCoreApplication,
3+
AgentCoreMcp,
4+
type AgentCoreProjectSpec,
5+
type McpSpec,
6+
type McpDeployedState,
7+
} from '@aws/agentcore-cdk';
28
import { CfnOutput, Stack, type StackProps } from 'aws-cdk-lib';
39
import { Construct } from 'constructs';
410

@@ -7,6 +13,14 @@ export interface AgentCoreStackProps extends StackProps {
713
* The AgentCore project specification containing agents, memories, and credentials.
814
*/
915
spec: AgentCoreProjectSpec;
16+
/**
17+
* The MCP specification containing gateways and servers.
18+
*/
19+
mcpSpec?: McpSpec;
20+
/**
21+
* The MCP deployed state.
22+
*/
23+
mcpDeployedState?: McpDeployedState;
1024
}
1125

1226
/**
@@ -22,13 +36,22 @@ export class AgentCoreStack extends Stack {
2236
constructor(scope: Construct, id: string, props: AgentCoreStackProps) {
2337
super(scope, id, props);
2438

25-
const { spec } = props;
39+
const { spec, mcpSpec, mcpDeployedState } = props;
2640

2741
// Create AgentCoreApplication with all agents
2842
this.application = new AgentCoreApplication(this, 'Application', {
2943
spec,
3044
});
3145

46+
// Create AgentCoreMcp if there are gateways configured
47+
if (mcpSpec?.agentCoreGateways && mcpSpec.agentCoreGateways.length > 0) {
48+
new AgentCoreMcp(this, 'Mcp', {
49+
spec: mcpSpec,
50+
deployedState: mcpDeployedState,
51+
application: this.application,
52+
});
53+
}
54+
3255
// Stack-level output
3356
new CfnOutput(this, 'StackNameOutput', {
3457
description: 'Name of the CloudFormation Stack',

src/cli/cloudformation/__tests__/outputs-extended.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ describe('buildDeployedState', () => {
157157
},
158158
};
159159

160-
const state = buildDeployedState('default', 'MyStack', agents);
160+
const state = buildDeployedState('default', 'MyStack', agents, {});
161161
expect(state.targets.default).toBeDefined();
162162
expect(state.targets.default!.resources?.agents).toEqual(agents);
163163
expect(state.targets.default!.resources?.stackName).toBe('MyStack');
@@ -181,7 +181,7 @@ describe('buildDeployedState', () => {
181181
DevAgent: { runtimeId: 'rt-d', runtimeArn: 'arn:rt-d', roleArn: 'arn:role-d' },
182182
};
183183

184-
const state = buildDeployedState('dev', 'DevStack', devAgents, existing);
184+
const state = buildDeployedState('dev', 'DevStack', devAgents, {}, existing);
185185
expect(state.targets.prod).toBeDefined();
186186
expect(state.targets.dev).toBeDefined();
187187
expect(state.targets.prod!.resources?.stackName).toBe('ProdStack');
@@ -197,22 +197,22 @@ describe('buildDeployedState', () => {
197197
},
198198
};
199199

200-
const state = buildDeployedState('default', 'NewStack', {}, existing);
200+
const state = buildDeployedState('default', 'NewStack', {}, {}, existing);
201201
expect(state.targets.default!.resources?.stackName).toBe('NewStack');
202202
});
203203

204204
it('includes identityKmsKeyArn when provided', () => {
205-
const state = buildDeployedState('default', 'Stack', {}, undefined, 'arn:aws:kms:key');
205+
const state = buildDeployedState('default', 'Stack', {}, {}, undefined, 'arn:aws:kms:key');
206206
expect(state.targets.default!.resources?.identityKmsKeyArn).toBe('arn:aws:kms:key');
207207
});
208208

209209
it('omits identityKmsKeyArn when undefined', () => {
210-
const state = buildDeployedState('default', 'Stack', {});
210+
const state = buildDeployedState('default', 'Stack', {}, {});
211211
expect(state.targets.default!.resources?.identityKmsKeyArn).toBeUndefined();
212212
});
213213

214214
it('handles empty agents record', () => {
215-
const state = buildDeployedState('default', 'Stack', {});
215+
const state = buildDeployedState('default', 'Stack', {}, {});
216216
expect(state.targets.default!.resources?.agents).toEqual({});
217217
});
218218
});

src/cli/cloudformation/__tests__/outputs.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ describe('buildDeployedState', () => {
1515
'default',
1616
'TestStack',
1717
agents,
18+
{},
1819
undefined,
1920
'arn:aws:kms:us-east-1:123456789012:key/abc-123'
2021
);
@@ -31,7 +32,7 @@ describe('buildDeployedState', () => {
3132
},
3233
};
3334

34-
const result = buildDeployedState('default', 'TestStack', agents);
35+
const result = buildDeployedState('default', 'TestStack', agents, {});
3536

3637
expect(result.targets.default!.resources?.identityKmsKeyArn).toBeUndefined();
3738
});
@@ -52,6 +53,7 @@ describe('buildDeployedState', () => {
5253
'dev',
5354
'DevStack',
5455
{},
56+
{},
5557
existingState,
5658
'arn:aws:kms:us-east-1:123456789012:key/dev-key'
5759
);

src/cli/cloudformation/outputs.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,47 @@ export async function getStackOutputs(region: string, stackName: string): Promis
2626
return outputs;
2727
}
2828

29+
/**
30+
* Parse stack outputs into deployed state for gateways.
31+
*
32+
* Output key pattern for gateways:
33+
* Gateway{GatewayName}UrlOutput{Hash}
34+
*
35+
* Examples:
36+
* - GatewayMyGatewayUrlOutput3E11FAB4
37+
*/
38+
export function parseGatewayOutputs(
39+
outputs: StackOutputs,
40+
gatewaySpecs: Record<string, unknown>
41+
): Record<string, { gatewayId: string; gatewayArn: string }> {
42+
const gateways: Record<string, { gatewayId: string; gatewayArn: string }> = {};
43+
44+
// Map PascalCase gateway names to original names for lookup
45+
const gatewayNames = Object.keys(gatewaySpecs);
46+
const gatewayIdMap = new Map(gatewayNames.map(name => [toPascalId(name), name]));
47+
48+
// Match pattern: Gateway{GatewayName}UrlOutput
49+
const outputPattern = /^Gateway(.+?)UrlOutput/;
50+
51+
for (const [key, value] of Object.entries(outputs)) {
52+
const match = outputPattern.exec(key);
53+
if (!match) continue;
54+
55+
const logicalGateway = match[1];
56+
if (!logicalGateway) continue;
57+
58+
// Look up original gateway name from PascalCase version
59+
const gatewayName = gatewayIdMap.get(logicalGateway) ?? logicalGateway;
60+
61+
gateways[gatewayName] = {
62+
gatewayId: gatewayName,
63+
gatewayArn: value,
64+
};
65+
}
66+
67+
return gateways;
68+
}
69+
2970
/**
3071
* Parse stack outputs into deployed state for agents.
3172
*
@@ -132,6 +173,7 @@ export function buildDeployedState(
132173
targetName: string,
133174
stackName: string,
134175
agents: Record<string, AgentCoreDeployedState>,
176+
gateways: Record<string, { gatewayId: string; gatewayArn: string }>,
135177
existingState?: DeployedState,
136178
identityKmsKeyArn?: string
137179
): DeployedState {
@@ -143,6 +185,13 @@ export function buildDeployedState(
143185
},
144186
};
145187

188+
// Add MCP state if gateways exist
189+
if (Object.keys(gateways).length > 0) {
190+
targetState.resources!.mcp = {
191+
gateways,
192+
};
193+
}
194+
146195
return {
147196
targets: {
148197
...existingState?.targets,

src/cli/commands/add/__tests__/validate.test.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ describe('validate', () => {
237237

238238
describe('validateAddGatewayTargetOptions', () => {
239239
// AC15: Required fields validated
240-
it('returns error for missing required fields', () => {
240+
it('returns error for missing required fields', async () => {
241241
const requiredFields: { field: keyof AddGatewayTargetOptions; error: string }[] = [
242242
{ field: 'name', error: '--name is required' },
243243
{ field: 'language', error: '--language is required' },
@@ -246,44 +246,49 @@ describe('validate', () => {
246246

247247
for (const { field, error } of requiredFields) {
248248
const opts = { ...validGatewayTargetOptionsMcpRuntime, [field]: undefined };
249-
const result = validateAddGatewayTargetOptions(opts);
249+
const result = await validateAddGatewayTargetOptions(opts);
250250
expect(result.valid, `Should fail for missing ${String(field)}`).toBe(false);
251251
expect(result.error).toBe(error);
252252
}
253253
});
254254

255255
// AC16: Invalid values rejected
256-
it('returns error for invalid values', () => {
257-
let result = validateAddGatewayTargetOptions({ ...validGatewayTargetOptionsMcpRuntime, language: 'Java' as any });
256+
it('returns error for invalid values', async () => {
257+
let result = await validateAddGatewayTargetOptions({
258+
...validGatewayTargetOptionsMcpRuntime,
259+
language: 'Java' as any,
260+
});
258261
expect(result.valid).toBe(false);
259262
expect(result.error?.includes('Invalid language')).toBeTruthy();
260263

261-
result = validateAddGatewayTargetOptions({ ...validGatewayTargetOptionsMcpRuntime, exposure: 'invalid' as any });
264+
result = await validateAddGatewayTargetOptions({
265+
...validGatewayTargetOptionsMcpRuntime,
266+
exposure: 'invalid' as any,
267+
});
262268
expect(result.valid).toBe(false);
263269
expect(result.error?.includes('Invalid exposure')).toBeTruthy();
264270
});
265271

266272
// AC17: mcp-runtime exposure requires agents
267-
it('returns error for mcp-runtime without agents', () => {
268-
let result = validateAddGatewayTargetOptions({ ...validGatewayTargetOptionsMcpRuntime, agents: undefined });
273+
it('returns error for mcp-runtime without agents', async () => {
274+
let result = await validateAddGatewayTargetOptions({ ...validGatewayTargetOptionsMcpRuntime, agents: undefined });
269275
expect(result.valid).toBe(false);
270276
expect(result.error).toBe('--agents is required for mcp-runtime exposure');
271277

272-
result = validateAddGatewayTargetOptions({ ...validGatewayTargetOptionsMcpRuntime, agents: ',,,' });
278+
result = await validateAddGatewayTargetOptions({ ...validGatewayTargetOptionsMcpRuntime, agents: ',,,' });
273279
expect(result.valid).toBe(false);
274280
expect(result.error).toBe('At least one agent is required');
275281
});
276282

277-
// AC18: behind-gateway exposure is disabled (coming soon)
278-
it('returns coming soon error for behind-gateway exposure', () => {
279-
const result = validateAddGatewayTargetOptions({ ...validGatewayTargetOptionsBehindGateway });
280-
expect(result.valid).toBe(false);
281-
expect(result.error).toContain('coming soon');
283+
// AC18: behind-gateway exposure is enabled
284+
it('passes for valid behind-gateway options', async () => {
285+
const result = await validateAddGatewayTargetOptions({ ...validGatewayTargetOptionsBehindGateway });
286+
expect(result.valid).toBe(true);
282287
});
283288

284289
// AC19: Valid options pass
285-
it('passes for valid mcp-runtime options', () => {
286-
expect(validateAddGatewayTargetOptions(validGatewayTargetOptionsMcpRuntime)).toEqual({ valid: true });
290+
it('passes for valid mcp-runtime options', async () => {
291+
expect(await validateAddGatewayTargetOptions(validGatewayTargetOptionsMcpRuntime)).toEqual({ valid: true });
287292
});
288293
});
289294

0 commit comments

Comments
 (0)