Skip to content

Commit 23df9fe

Browse files
authored
feat!: merge mcp.json into agentcore.json (#605)
* feat!: merge mcp.json into agentcore.json * fix: use type-safe cast in vended cdk.ts for MCP fields The vended CDK project reads gateway fields from agentcore.json but the published @aws/agentcore-cdk type doesn't include them yet. Use 'as any' cast for forward-compatibility. * fix: address PR review feedback for mcp-into-agentcore-json merge - Use SaveDocumentResult type instead of inline { ok, error } in McpGuidedEditor - Use Pick<AgentCoreMcpSpec, 'agentCoreGateways'> instead of inline type in deploy actions - Add uniqueBy validation to agentCoreGateways, mcpRuntimeTools, unassignedTargets arrays - Add .strict() to AgentCoreProjectSpecSchema to reject unknown fields
1 parent b941fbc commit 23df9fe

56 files changed

Lines changed: 578 additions & 635 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

integ-tests/add-remove-gateway.test.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
44
import { join } from 'node:path';
55
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
66

7-
async function readMcpConfig(projectPath: string) {
8-
return JSON.parse(await readFile(join(projectPath, 'agentcore/mcp.json'), 'utf-8'));
7+
async function readProjectConfig(projectPath: string) {
8+
return JSON.parse(await readFile(join(projectPath, 'agentcore/agentcore.json'), 'utf-8'));
99
}
1010

1111
describe('integration: add and remove gateway with external MCP server', () => {
@@ -29,9 +29,9 @@ describe('integration: add and remove gateway with external MCP server', () => {
2929
const json = JSON.parse(result.stdout);
3030
expect(json.success).toBe(true);
3131

32-
const mcpSpec = await readMcpConfig(project.projectPath);
32+
const mcpSpec = await readProjectConfig(project.projectPath);
3333
const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName);
34-
expect(gateway, `Gateway "${gatewayName}" should be in mcp.json`).toBeTruthy();
34+
expect(gateway, `Gateway "${gatewayName}" should be in agentcore.json`).toBeTruthy();
3535
expect(gateway.authorizerType).toBe('NONE');
3636
});
3737

@@ -57,7 +57,7 @@ describe('integration: add and remove gateway with external MCP server', () => {
5757
const json = JSON.parse(result.stdout);
5858
expect(json.success).toBe(true);
5959

60-
const mcpSpec = await readMcpConfig(project.projectPath);
60+
const mcpSpec = await readProjectConfig(project.projectPath);
6161
const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName);
6262
const target = gateway?.targets?.find((t: { name: string }) => t.name === targetName);
6363
expect(target, `Target "${targetName}" should be in gateway targets`).toBeTruthy();
@@ -70,7 +70,7 @@ describe('integration: add and remove gateway with external MCP server', () => {
7070
const json = JSON.parse(result.stdout);
7171
expect(json.success).toBe(true);
7272

73-
const mcpSpec = await readMcpConfig(project.projectPath);
73+
const mcpSpec = await readProjectConfig(project.projectPath);
7474
const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName);
7575
const targets = gateway?.targets ?? [];
7676
const found = targets.find((t: { name: string }) => t.name === targetName);
@@ -84,7 +84,7 @@ describe('integration: add and remove gateway with external MCP server', () => {
8484
const json = JSON.parse(result.stdout);
8585
expect(json.success).toBe(true);
8686

87-
const mcpSpec = await readMcpConfig(project.projectPath);
87+
const mcpSpec = await readProjectConfig(project.projectPath);
8888
const gateways = mcpSpec.agentCoreGateways ?? [];
8989
const found = gateways.find((g: { name: string }) => g.name === gatewayName);
9090
expect(found, `Gateway "${gatewayName}" should be removed`).toBeFalsy();
@@ -161,7 +161,7 @@ describe('integration: add and remove gateway with OpenAPI schema target', () =>
161161
expect(json.success).toBe(true);
162162
expect(json.toolName).toBe(targetName);
163163

164-
const mcpSpec = await readMcpConfig(project.projectPath);
164+
const mcpSpec = await readProjectConfig(project.projectPath);
165165
const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName);
166166
const target = gateway?.targets?.find((t: { name: string }) => t.name === targetName);
167167
expect(target, `Target "${targetName}" should be in gateway targets`).toBeTruthy();
@@ -202,7 +202,7 @@ describe('integration: add and remove gateway with OpenAPI schema target', () =>
202202
const json = JSON.parse(result.stdout);
203203
expect(json.success).toBe(true);
204204

205-
const mcpSpec = await readMcpConfig(project.projectPath);
205+
const mcpSpec = await readProjectConfig(project.projectPath);
206206
const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName);
207207
const targets = gateway?.targets ?? [];
208208
const found = targets.find((t: { name: string }) => t.name === targetName);
@@ -271,7 +271,7 @@ describe('integration: add gateway with S3 URI schema target', () => {
271271
expect(json.success).toBe(true);
272272
expect(json.toolName).toBe(targetName);
273273

274-
const mcpSpec = await readMcpConfig(project.projectPath);
274+
const mcpSpec = await readProjectConfig(project.projectPath);
275275
const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName);
276276
const target = gateway?.targets?.find((t: { name: string }) => t.name === targetName);
277277
expect(target, `Target "${targetName}" should be in gateway targets`).toBeTruthy();
@@ -333,7 +333,7 @@ describe('integration: add gateway with S3 URI and bucketOwnerAccountId', () =>
333333
const json = JSON.parse(result.stdout);
334334
expect(json.success).toBe(true);
335335

336-
const mcpSpec = await readMcpConfig(project.projectPath);
336+
const mcpSpec = await readProjectConfig(project.projectPath);
337337
const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName);
338338
const target = gateway?.targets?.find((t: { name: string }) => t.name === targetName);
339339
expect(target.schemaSource?.s3?.uri).toBe('s3://cross-account-bucket/spec.json');
@@ -395,7 +395,7 @@ describe('integration: add gateway with Smithy model target', () => {
395395
expect(json.success).toBe(true);
396396
expect(json.toolName).toBe(targetName);
397397

398-
const mcpSpec = await readMcpConfig(project.projectPath);
398+
const mcpSpec = await readProjectConfig(project.projectPath);
399399
const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName);
400400
const target = gateway?.targets?.find((t: { name: string }) => t.name === targetName);
401401
expect(target, `Target "${targetName}" should be in gateway targets`).toBeTruthy();
@@ -410,7 +410,7 @@ describe('integration: add gateway with Smithy model target', () => {
410410
const json = JSON.parse(result.stdout);
411411
expect(json.success).toBe(true);
412412

413-
const mcpSpec = await readMcpConfig(project.projectPath);
413+
const mcpSpec = await readProjectConfig(project.projectPath);
414414
const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName);
415415
const targets = gateway?.targets ?? [];
416416
const found = targets.find((t: { name: string }) => t.name === targetName);

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

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,19 @@ async function main() {
7373
const spec = await configIO.readProjectSpec();
7474
const targets = await configIO.readAWSDeploymentTargets();
7575
76-
// Read MCP configuration if it exists
77-
let mcpSpec;
78-
try {
79-
mcpSpec = await configIO.readMcpSpec();
80-
} catch {
81-
// MCP config is optional
82-
}
76+
// Extract MCP configuration from project spec.
77+
// Gateway fields are stored in agentcore.json but may not yet be on the
78+
// AgentCoreProjectSpec type from @aws/agentcore-cdk, so we read them
79+
// dynamically and cast the resulting object.
80+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
81+
const specAny = spec as any;
82+
const mcpSpec = specAny.agentCoreGateways?.length
83+
? {
84+
agentCoreGateways: specAny.agentCoreGateways,
85+
mcpRuntimeTools: specAny.mcpRuntimeTools,
86+
unassignedTargets: specAny.unassignedTargets,
87+
}
88+
: undefined;
8389
8490
// Read deployed state for credential ARNs (populated by pre-deploy identity setup)
8591
let deployedState: Record<string, unknown> | undefined;

src/assets/cdk/bin/cdk.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,19 @@ async function main() {
2828
const spec = await configIO.readProjectSpec();
2929
const targets = await configIO.readAWSDeploymentTargets();
3030

31-
// Read MCP configuration if it exists
32-
let mcpSpec;
33-
try {
34-
mcpSpec = await configIO.readMcpSpec();
35-
} catch {
36-
// MCP config is optional
37-
}
31+
// Extract MCP configuration from project spec.
32+
// Gateway fields are stored in agentcore.json but may not yet be on the
33+
// AgentCoreProjectSpec type from @aws/agentcore-cdk, so we read them
34+
// dynamically and cast the resulting object.
35+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
36+
const specAny = spec as any;
37+
const mcpSpec = specAny.agentCoreGateways?.length
38+
? {
39+
agentCoreGateways: specAny.agentCoreGateways,
40+
mcpRuntimeTools: specAny.mcpRuntimeTools,
41+
unassignedTargets: specAny.unassignedTargets,
42+
}
43+
: undefined;
3844

3945
// Read deployed state for credential ARNs (populated by pre-deploy identity setup)
4046
let deployedState: Record<string, unknown> | undefined;

src/cli/commands/add/__tests__/add-gateway-target.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@ describe('add gateway-target command', () => {
7878
const json = JSON.parse(result.stdout);
7979
expect(json.success).toBe(true);
8080

81-
// Verify in mcp.json
82-
const mcpSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/mcp.json'), 'utf-8'));
83-
const gateway = mcpSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName);
81+
// Verify in agentcore.json
82+
const projectSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/agentcore.json'), 'utf-8'));
83+
const gateway = projectSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName);
8484
const target = gateway?.targets?.find((t: { name: string }) => t.name === targetName);
8585
expect(target, 'Target should be in gateway targets').toBeTruthy();
8686
});
@@ -122,8 +122,8 @@ describe('add gateway-target command', () => {
122122
expect(json.success).toBe(true);
123123
expect(json.toolName).toBe(targetName);
124124

125-
const mcpSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/mcp.json'), 'utf-8'));
126-
const gateway = mcpSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName);
125+
const projectSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/agentcore.json'), 'utf-8'));
126+
const gateway = projectSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName);
127127
const target = gateway?.targets?.find((t: { name: string }) => t.name === targetName);
128128
expect(target).toBeTruthy();
129129
expect(target.targetType).toBe('lambdaFunctionArn');

src/cli/commands/add/__tests__/add-gateway.test.ts

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ describe('add gateway command', () => {
3737
expect(json.success).toBe(true);
3838
expect(json.gatewayName).toBe(gatewayName);
3939

40-
// Verify gateway in mcp.json
41-
const mcpSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/mcp.json'), 'utf-8'));
42-
const gateway = mcpSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName);
43-
expect(gateway, 'Gateway should be in mcp.json').toBeTruthy();
40+
// Verify gateway in agentcore.json
41+
const projectSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/agentcore.json'), 'utf-8'));
42+
const gateway = projectSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName);
43+
expect(gateway, 'Gateway should be in agentcore.json').toBeTruthy();
4444
expect(gateway.authorizerType).toBe('NONE');
4545
});
4646

@@ -105,10 +105,10 @@ describe('add gateway command', () => {
105105
const json = JSON.parse(result.stdout);
106106
expect(json.success).toBe(true);
107107

108-
// Verify JWT config in mcp.json
109-
const mcpSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/mcp.json'), 'utf-8'));
110-
const gateway = mcpSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName);
111-
expect(gateway, 'Gateway should be in mcp.json').toBeTruthy();
108+
// Verify JWT config in agentcore.json
109+
const projectSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/agentcore.json'), 'utf-8'));
110+
const gateway = projectSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName);
111+
expect(gateway, 'Gateway should be in agentcore.json').toBeTruthy();
112112
expect(gateway.authorizerType).toBe('CUSTOM_JWT');
113113
expect(gateway.authorizerConfiguration?.customJwtAuthorizer, 'Should have JWT config').toBeTruthy();
114114
});
@@ -180,15 +180,14 @@ describe('add gateway command', () => {
180180
const json = JSON.parse(result.stdout);
181181
expect(json.success).toBe(true);
182182

183-
// Verify allowedScopes in mcp.json
184-
const mcpSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/mcp.json'), 'utf-8'));
185-
const gateway = mcpSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName);
186-
expect(gateway, 'Gateway should be in mcp.json').toBeTruthy();
183+
// Verify allowedScopes in agentcore.json
184+
const projectSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/agentcore.json'), 'utf-8'));
185+
const gateway = projectSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName);
186+
expect(gateway, 'Gateway should be in agentcore.json').toBeTruthy();
187187
expect(gateway.authorizerType).toBe('CUSTOM_JWT');
188188
expect(gateway.authorizerConfiguration?.customJwtAuthorizer?.allowedScopes).toEqual(['scope1', 'scope2']);
189189

190190
// Verify managed OAuth credential in agentcore.json
191-
const projectSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/agentcore.json'), 'utf-8'));
192191
const credential = projectSpec.credentials.find((c: { name: string }) => c.name === `${gatewayName}-oauth`);
193192
expect(credential, 'Managed OAuth credential should exist').toBeTruthy();
194193
expect(credential.type).toBe('OAuthCredentialProvider');

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
1717

1818
const mockReadProjectSpec = vi.fn();
1919
const mockConfigExists = vi.fn().mockReturnValue(true);
20-
const mockReadMcpSpec = vi.fn();
2120

2221
vi.mock('../../../../lib/index.js', () => ({
2322
ConfigIO: class {
2423
readProjectSpec = mockReadProjectSpec;
2524
configExists = mockConfigExists;
26-
readMcpSpec = mockReadMcpSpec;
2725
},
2826
findConfigRoot: vi.fn().mockReturnValue('/mock/project/agentcore'),
2927
}));
@@ -337,7 +335,7 @@ describe('validate', () => {
337335
describe('validateAddGatewayTargetOptions', () => {
338336
beforeEach(() => {
339337
// By default, mock that the gateway from validGatewayTargetOptions exists
340-
mockReadMcpSpec.mockResolvedValue({ agentCoreGateways: [{ name: 'my-gateway' }] });
338+
mockReadProjectSpec.mockResolvedValue({ agentCoreGateways: [{ name: 'my-gateway' }] });
341339
});
342340

343341
// AC15: Required fields validated
@@ -356,15 +354,15 @@ describe('validate', () => {
356354
});
357355

358356
it('returns error when no gateways exist', async () => {
359-
mockReadMcpSpec.mockResolvedValue({ agentCoreGateways: [] });
357+
mockReadProjectSpec.mockResolvedValue({ agentCoreGateways: [] });
360358
const result = await validateAddGatewayTargetOptions({ ...validGatewayTargetOptions });
361359
expect(result.valid).toBe(false);
362360
expect(result.error).toContain('No gateways found');
363361
expect(result.error).toContain('agentcore add gateway');
364362
});
365363

366364
it('returns error when specified gateway does not exist', async () => {
367-
mockReadMcpSpec.mockResolvedValue({ agentCoreGateways: [{ name: 'other-gateway' }] });
365+
mockReadProjectSpec.mockResolvedValue({ agentCoreGateways: [{ name: 'other-gateway' }] });
368366
const result = await validateAddGatewayTargetOptions({ ...validGatewayTargetOptions });
369367
expect(result.valid).toBe(false);
370368
expect(result.error).toContain('Gateway "my-gateway" not found');
@@ -470,6 +468,7 @@ describe('validate', () => {
470468
// AC21: credential validation through outbound auth
471469
it('returns error when credential not found', async () => {
472470
mockReadProjectSpec.mockResolvedValue({
471+
agentCoreGateways: [{ name: 'my-gateway' }],
473472
credentials: [{ name: 'existing-cred', type: 'ApiKey' }],
474473
});
475474

@@ -488,6 +487,7 @@ describe('validate', () => {
488487

489488
it('returns error when no credentials configured', async () => {
490489
mockReadProjectSpec.mockResolvedValue({
490+
agentCoreGateways: [{ name: 'my-gateway' }],
491491
credentials: [],
492492
});
493493

@@ -506,6 +506,7 @@ describe('validate', () => {
506506

507507
it('passes when credential exists', async () => {
508508
mockReadProjectSpec.mockResolvedValue({
509+
agentCoreGateways: [{ name: 'my-gateway' }],
509510
credentials: [{ name: 'valid-cred', type: 'ApiKey' }],
510511
});
511512

src/cli/commands/add/validate.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -370,10 +370,8 @@ export async function validateAddGatewayTargetOptions(options: AddGatewayTargetO
370370
const gatewayConfigIO = new ConfigIO();
371371
let existingGateways: string[] = [];
372372
try {
373-
if (gatewayConfigIO.configExists('mcp')) {
374-
const mcpSpec = await gatewayConfigIO.readMcpSpec();
375-
existingGateways = mcpSpec.agentCoreGateways.map(g => g.name);
376-
}
373+
const project = await gatewayConfigIO.readProjectSpec();
374+
existingGateways = project.agentCoreGateways.map(g => g.name);
377375
} catch {
378376
// If we can't read the config, treat as no gateways
379377
}

src/cli/commands/create/action.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ function createDefaultProjectSpec(projectName: string): AgentCoreProjectSpec {
3333
credentials: [],
3434
evaluators: [],
3535
onlineEvalConfigs: [],
36+
agentCoreGateways: [],
3637
policyEngines: [],
3738
};
3839
}

src/cli/commands/deploy/actions.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ConfigIO, SecureCredentials } from '../../../lib';
2-
import type { DeployedState } from '../../../schema';
2+
import type { AgentCoreMcpSpec, DeployedState } from '../../../schema';
33
import { validateAwsCredentials } from '../../aws/account';
44
import { createSwitchableIoHost } from '../../cdk/toolkit-lib';
55
import {
@@ -81,13 +81,13 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise<Dep
8181
}
8282
endStep('success');
8383

84-
// Read MCP spec for gateway information
85-
let mcpSpec;
84+
// Read project spec for gateway information (used later for deploy step name and outputs)
85+
let mcpSpec: Pick<AgentCoreMcpSpec, 'agentCoreGateways'> | null = null;
8686
try {
87-
mcpSpec = await configIO.readMcpSpec();
87+
const projectSpec = await configIO.readProjectSpec();
88+
mcpSpec = { agentCoreGateways: projectSpec.agentCoreGateways };
8889
} catch {
89-
// No mcp.json or invalid — no gateways
90-
mcpSpec = null;
90+
// Project read failed — no gateways
9191
}
9292

9393
// Preflight: validate project

src/cli/commands/logs/__tests__/action.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ describe('resolveAgentContext', () => {
5858
credentials: [],
5959
evaluators: [],
6060
onlineEvalConfigs: [],
61+
agentCoreGateways: [],
6162
policyEngines: [],
6263
},
6364
deployedState: {
@@ -119,6 +120,7 @@ describe('resolveAgentContext', () => {
119120
credentials: [],
120121
evaluators: [],
121122
onlineEvalConfigs: [],
123+
agentCoreGateways: [],
122124
policyEngines: [],
123125
},
124126
});
@@ -160,6 +162,7 @@ describe('resolveAgentContext', () => {
160162
credentials: [],
161163
evaluators: [],
162164
onlineEvalConfigs: [],
165+
agentCoreGateways: [],
163166
policyEngines: [],
164167
},
165168
deployedState: {
@@ -209,6 +212,7 @@ describe('resolveAgentContext', () => {
209212
credentials: [],
210213
evaluators: [],
211214
onlineEvalConfigs: [],
215+
agentCoreGateways: [],
212216
policyEngines: [],
213217
},
214218
});

0 commit comments

Comments
 (0)