Skip to content

Commit a82a033

Browse files
committed
feat: enable gateway commands, UI updates, deploy pipeline, credential validation
1 parent d74fec3 commit a82a033

File tree

25 files changed

+543
-133
lines changed

25 files changed

+543
-133
lines changed

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

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { AgentCoreStack } from '../lib/cdk-stack';
3636
import { ConfigIO, type AwsDeploymentTarget } from '@aws/agentcore-cdk';
3737
import { App, type Environment } from 'aws-cdk-lib';
3838
import * as path from 'path';
39+
import * as fs from 'fs';
3940
4041
function toEnvironment(target: AwsDeploymentTarget): Environment {
4142
return {
@@ -56,6 +57,16 @@ async function main() {
5657
const spec = await configIO.readProjectSpec();
5758
const targets = await configIO.readAWSDeploymentTargets();
5859
60+
// Read MCP configuration if it exists
61+
let mcpSpec;
62+
let mcpDeployedState;
63+
try {
64+
mcpSpec = await configIO.readMcpSpec();
65+
mcpDeployedState = JSON.parse(fs.readFileSync(path.join(configRoot, '.cli', 'deployed-state.json'), 'utf8')).mcp;
66+
} catch {
67+
// MCP config is optional
68+
}
69+
5970
if (targets.length === 0) {
6071
throw new Error('No deployment targets configured. Please define targets in agentcore/aws-targets.json');
6172
}
@@ -68,6 +79,8 @@ async function main() {
6879
6980
new AgentCoreStack(app, stackName, {
7081
spec,
82+
mcpSpec,
83+
mcpDeployedState,
7184
env,
7285
description: \`AgentCore stack for \${spec.name} deployed to \${target.name} (\${target.region})\`,
7386
tags: {
@@ -203,7 +216,13 @@ exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/jest.config.js should
203216
`;
204217
205218
exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/lib/cdk-stack.ts should match snapshot 1`] = `
206-
"import { AgentCoreApplication, type AgentCoreProjectSpec } from '@aws/agentcore-cdk';
219+
"import {
220+
AgentCoreApplication,
221+
AgentCoreMcp,
222+
type AgentCoreProjectSpec,
223+
type McpSpec,
224+
type McpDeployedState,
225+
} from '@aws/agentcore-cdk';
207226
import { CfnOutput, Stack, type StackProps } from 'aws-cdk-lib';
208227
import { Construct } from 'constructs';
209228
@@ -212,6 +231,14 @@ export interface AgentCoreStackProps extends StackProps {
212231
* The AgentCore project specification containing agents, memories, and credentials.
213232
*/
214233
spec: AgentCoreProjectSpec;
234+
/**
235+
* The MCP specification containing gateways and servers.
236+
*/
237+
mcpSpec?: McpSpec;
238+
/**
239+
* The MCP deployed state.
240+
*/
241+
mcpDeployedState?: McpDeployedState;
215242
}
216243
217244
/**
@@ -227,13 +254,22 @@ export class AgentCoreStack extends Stack {
227254
constructor(scope: Construct, id: string, props: AgentCoreStackProps) {
228255
super(scope, id, props);
229256
230-
const { spec } = props;
257+
const { spec, mcpSpec, mcpDeployedState } = props;
231258
232259
// Create AgentCoreApplication with all agents
233260
this.application = new AgentCoreApplication(this, 'Application', {
234261
spec,
235262
});
236263
264+
// Create AgentCoreMcp if there are gateways configured
265+
if (mcpSpec?.agentCoreGateways && mcpSpec.agentCoreGateways.length > 0) {
266+
new AgentCoreMcp(this, 'Mcp', {
267+
spec: mcpSpec,
268+
deployedState: mcpDeployedState,
269+
application: this.application,
270+
});
271+
}
272+
237273
// Stack-level output
238274
new CfnOutput(this, 'StackNameOutput', {
239275
description: 'Name of the CloudFormation Stack',

src/assets/cdk/bin/cdk.ts

Lines changed: 13 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 {
@@ -23,6 +24,16 @@ async function main() {
2324
const spec = await configIO.readProjectSpec();
2425
const targets = await configIO.readAWSDeploymentTargets();
2526

27+
// Read MCP configuration if it exists
28+
let mcpSpec;
29+
let mcpDeployedState;
30+
try {
31+
mcpSpec = await configIO.readMcpSpec();
32+
mcpDeployedState = JSON.parse(fs.readFileSync(path.join(configRoot, '.cli', 'deployed-state.json'), 'utf8')).mcp;
33+
} catch {
34+
// MCP config is optional
35+
}
36+
2637
if (targets.length === 0) {
2738
throw new Error('No deployment targets configured. Please define targets in agentcore/aws-targets.json');
2839
}
@@ -35,6 +46,8 @@ async function main() {
3546

3647
new AgentCoreStack(app, stackName, {
3748
spec,
49+
mcpSpec,
50+
mcpDeployedState,
3851
env,
3952
description: `AgentCore stack for ${spec.name} deployed to ${target.name} (${target.region})`,
4053
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)