Skip to content

Commit 1955261

Browse files
committed
feat: enable gateway commands, UI updates, deploy pipeline, credential validation
1 parent 4a5924d commit 1955261

File tree

24 files changed

+489
-132
lines changed

24 files changed

+489
-132
lines changed

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

Lines changed: 32 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,7 @@ 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 { AgentCoreApplication, AgentCoreMcp, type AgentCoreProjectSpec, type McpSpec, type McpDeployedState } from '@aws/agentcore-cdk';
207220
import { CfnOutput, Stack, type StackProps } from 'aws-cdk-lib';
208221
import { Construct } from 'constructs';
209222
@@ -212,6 +225,14 @@ export interface AgentCoreStackProps extends StackProps {
212225
* The AgentCore project specification containing agents, memories, and credentials.
213226
*/
214227
spec: AgentCoreProjectSpec;
228+
/**
229+
* The MCP specification containing gateways and servers.
230+
*/
231+
mcpSpec?: McpSpec;
232+
/**
233+
* The MCP deployed state.
234+
*/
235+
mcpDeployedState?: McpDeployedState;
215236
}
216237
217238
/**
@@ -227,13 +248,22 @@ export class AgentCoreStack extends Stack {
227248
constructor(scope: Construct, id: string, props: AgentCoreStackProps) {
228249
super(scope, id, props);
229250
230-
const { spec } = props;
251+
const { spec, mcpSpec, mcpDeployedState } = props;
231252
232253
// Create AgentCoreApplication with all agents
233254
this.application = new AgentCoreApplication(this, 'Application', {
234255
spec,
235256
});
236257
258+
// Create AgentCoreMcp if there are gateways configured
259+
if (mcpSpec?.gateways && Object.keys(mcpSpec.gateways).length > 0) {
260+
new AgentCoreMcp(this, 'Mcp', {
261+
spec: mcpSpec,
262+
deployedState: mcpDeployedState,
263+
application: this.application,
264+
});
265+
}
266+
237267
// Stack-level output
238268
new CfnOutput(this, 'StackNameOutput', {
239269
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: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AgentCoreApplication, type AgentCoreProjectSpec } from '@aws/agentcore-cdk';
1+
import { AgentCoreApplication, AgentCoreMcp, type AgentCoreProjectSpec, type McpSpec, type McpDeployedState } from '@aws/agentcore-cdk';
22
import { CfnOutput, Stack, type StackProps } from 'aws-cdk-lib';
33
import { Construct } from 'constructs';
44

@@ -7,6 +7,14 @@ export interface AgentCoreStackProps extends StackProps {
77
* The AgentCore project specification containing agents, memories, and credentials.
88
*/
99
spec: AgentCoreProjectSpec;
10+
/**
11+
* The MCP specification containing gateways and servers.
12+
*/
13+
mcpSpec?: McpSpec;
14+
/**
15+
* The MCP deployed state.
16+
*/
17+
mcpDeployedState?: McpDeployedState;
1018
}
1119

1220
/**
@@ -22,13 +30,22 @@ export class AgentCoreStack extends Stack {
2230
constructor(scope: Construct, id: string, props: AgentCoreStackProps) {
2331
super(scope, id, props);
2432

25-
const { spec } = props;
33+
const { spec, mcpSpec, mcpDeployedState } = props;
2634

2735
// Create AgentCoreApplication with all agents
2836
this.application = new AgentCoreApplication(this, 'Application', {
2937
spec,
3038
});
3139

40+
// Create AgentCoreMcp if there are gateways configured
41+
if (mcpSpec?.gateways && Object.keys(mcpSpec.gateways).length > 0) {
42+
new AgentCoreMcp(this, 'Mcp', {
43+
spec: mcpSpec,
44+
deployedState: mcpDeployedState,
45+
application: this.application,
46+
});
47+
}
48+
3249
// Stack-level output
3350
new CfnOutput(this, 'StackNameOutput', {
3451
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: 14 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,43 @@ 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({ ...validGatewayTargetOptionsMcpRuntime, language: 'Java' as any });
258258
expect(result.valid).toBe(false);
259259
expect(result.error?.includes('Invalid language')).toBeTruthy();
260260

261-
result = validateAddGatewayTargetOptions({ ...validGatewayTargetOptionsMcpRuntime, exposure: 'invalid' as any });
261+
result = await validateAddGatewayTargetOptions({ ...validGatewayTargetOptionsMcpRuntime, exposure: 'invalid' as any });
262262
expect(result.valid).toBe(false);
263263
expect(result.error?.includes('Invalid exposure')).toBeTruthy();
264264
});
265265

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

272-
result = validateAddGatewayTargetOptions({ ...validGatewayTargetOptionsMcpRuntime, agents: ',,,' });
272+
result = await validateAddGatewayTargetOptions({ ...validGatewayTargetOptionsMcpRuntime, agents: ',,,' });
273273
expect(result.valid).toBe(false);
274274
expect(result.error).toBe('At least one agent is required');
275275
});
276276

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');
277+
// AC18: behind-gateway exposure is enabled
278+
it('passes for valid behind-gateway options', async () => {
279+
const result = await validateAddGatewayTargetOptions({ ...validGatewayTargetOptionsBehindGateway });
280+
expect(result.valid).toBe(true);
282281
});
283282

284283
// AC19: Valid options pass
285-
it('passes for valid mcp-runtime options', () => {
286-
expect(validateAddGatewayTargetOptions(validGatewayTargetOptionsMcpRuntime)).toEqual({ valid: true });
284+
it('passes for valid mcp-runtime options', async () => {
285+
expect(await validateAddGatewayTargetOptions(validGatewayTargetOptionsMcpRuntime)).toEqual({ valid: true });
287286
});
288287
});
289288

0 commit comments

Comments
 (0)