diff --git a/integ-tests/add-remove-gateway.test.ts b/integ-tests/add-remove-gateway.test.ts index 83a2a0221..66d153630 100644 --- a/integ-tests/add-remove-gateway.test.ts +++ b/integ-tests/add-remove-gateway.test.ts @@ -4,8 +4,8 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -async function readMcpConfig(projectPath: string) { - return JSON.parse(await readFile(join(projectPath, 'agentcore/mcp.json'), 'utf-8')); +async function readProjectConfig(projectPath: string) { + return JSON.parse(await readFile(join(projectPath, 'agentcore/agentcore.json'), 'utf-8')); } describe('integration: add and remove gateway with external MCP server', () => { @@ -29,9 +29,9 @@ describe('integration: add and remove gateway with external MCP server', () => { const json = JSON.parse(result.stdout); expect(json.success).toBe(true); - const mcpSpec = await readMcpConfig(project.projectPath); + const mcpSpec = await readProjectConfig(project.projectPath); const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName); - expect(gateway, `Gateway "${gatewayName}" should be in mcp.json`).toBeTruthy(); + expect(gateway, `Gateway "${gatewayName}" should be in agentcore.json`).toBeTruthy(); expect(gateway.authorizerType).toBe('NONE'); }); @@ -57,7 +57,7 @@ describe('integration: add and remove gateway with external MCP server', () => { const json = JSON.parse(result.stdout); expect(json.success).toBe(true); - const mcpSpec = await readMcpConfig(project.projectPath); + const mcpSpec = await readProjectConfig(project.projectPath); const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName); const target = gateway?.targets?.find((t: { name: string }) => t.name === targetName); expect(target, `Target "${targetName}" should be in gateway targets`).toBeTruthy(); @@ -70,7 +70,7 @@ describe('integration: add and remove gateway with external MCP server', () => { const json = JSON.parse(result.stdout); expect(json.success).toBe(true); - const mcpSpec = await readMcpConfig(project.projectPath); + const mcpSpec = await readProjectConfig(project.projectPath); const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName); const targets = gateway?.targets ?? []; const found = targets.find((t: { name: string }) => t.name === targetName); @@ -84,7 +84,7 @@ describe('integration: add and remove gateway with external MCP server', () => { const json = JSON.parse(result.stdout); expect(json.success).toBe(true); - const mcpSpec = await readMcpConfig(project.projectPath); + const mcpSpec = await readProjectConfig(project.projectPath); const gateways = mcpSpec.agentCoreGateways ?? []; const found = gateways.find((g: { name: string }) => g.name === gatewayName); expect(found, `Gateway "${gatewayName}" should be removed`).toBeFalsy(); @@ -161,7 +161,7 @@ describe('integration: add and remove gateway with OpenAPI schema target', () => expect(json.success).toBe(true); expect(json.toolName).toBe(targetName); - const mcpSpec = await readMcpConfig(project.projectPath); + const mcpSpec = await readProjectConfig(project.projectPath); const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName); const target = gateway?.targets?.find((t: { name: string }) => t.name === targetName); expect(target, `Target "${targetName}" should be in gateway targets`).toBeTruthy(); @@ -202,7 +202,7 @@ describe('integration: add and remove gateway with OpenAPI schema target', () => const json = JSON.parse(result.stdout); expect(json.success).toBe(true); - const mcpSpec = await readMcpConfig(project.projectPath); + const mcpSpec = await readProjectConfig(project.projectPath); const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName); const targets = gateway?.targets ?? []; const found = targets.find((t: { name: string }) => t.name === targetName); @@ -271,7 +271,7 @@ describe('integration: add gateway with S3 URI schema target', () => { expect(json.success).toBe(true); expect(json.toolName).toBe(targetName); - const mcpSpec = await readMcpConfig(project.projectPath); + const mcpSpec = await readProjectConfig(project.projectPath); const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName); const target = gateway?.targets?.find((t: { name: string }) => t.name === targetName); expect(target, `Target "${targetName}" should be in gateway targets`).toBeTruthy(); @@ -333,7 +333,7 @@ describe('integration: add gateway with S3 URI and bucketOwnerAccountId', () => const json = JSON.parse(result.stdout); expect(json.success).toBe(true); - const mcpSpec = await readMcpConfig(project.projectPath); + const mcpSpec = await readProjectConfig(project.projectPath); const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName); const target = gateway?.targets?.find((t: { name: string }) => t.name === targetName); expect(target.schemaSource?.s3?.uri).toBe('s3://cross-account-bucket/spec.json'); @@ -395,7 +395,7 @@ describe('integration: add gateway with Smithy model target', () => { expect(json.success).toBe(true); expect(json.toolName).toBe(targetName); - const mcpSpec = await readMcpConfig(project.projectPath); + const mcpSpec = await readProjectConfig(project.projectPath); const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName); const target = gateway?.targets?.find((t: { name: string }) => t.name === targetName); expect(target, `Target "${targetName}" should be in gateway targets`).toBeTruthy(); @@ -410,7 +410,7 @@ describe('integration: add gateway with Smithy model target', () => { const json = JSON.parse(result.stdout); expect(json.success).toBe(true); - const mcpSpec = await readMcpConfig(project.projectPath); + const mcpSpec = await readProjectConfig(project.projectPath); const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName); const targets = gateway?.targets ?? []; const found = targets.find((t: { name: string }) => t.name === targetName); diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index f77c935f5..ee1742f3b 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -73,13 +73,19 @@ async function main() { const spec = await configIO.readProjectSpec(); const targets = await configIO.readAWSDeploymentTargets(); - // Read MCP configuration if it exists - let mcpSpec; - try { - mcpSpec = await configIO.readMcpSpec(); - } catch { - // MCP config is optional - } + // Extract MCP configuration from project spec. + // Gateway fields are stored in agentcore.json but may not yet be on the + // AgentCoreProjectSpec type from @aws/agentcore-cdk, so we read them + // dynamically and cast the resulting object. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const specAny = spec as any; + const mcpSpec = specAny.agentCoreGateways?.length + ? { + agentCoreGateways: specAny.agentCoreGateways, + mcpRuntimeTools: specAny.mcpRuntimeTools, + unassignedTargets: specAny.unassignedTargets, + } + : undefined; // Read deployed state for credential ARNs (populated by pre-deploy identity setup) let deployedState: Record | undefined; diff --git a/src/assets/cdk/bin/cdk.ts b/src/assets/cdk/bin/cdk.ts index 9b23d57d5..7a78b71cd 100644 --- a/src/assets/cdk/bin/cdk.ts +++ b/src/assets/cdk/bin/cdk.ts @@ -28,13 +28,19 @@ async function main() { const spec = await configIO.readProjectSpec(); const targets = await configIO.readAWSDeploymentTargets(); - // Read MCP configuration if it exists - let mcpSpec; - try { - mcpSpec = await configIO.readMcpSpec(); - } catch { - // MCP config is optional - } + // Extract MCP configuration from project spec. + // Gateway fields are stored in agentcore.json but may not yet be on the + // AgentCoreProjectSpec type from @aws/agentcore-cdk, so we read them + // dynamically and cast the resulting object. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const specAny = spec as any; + const mcpSpec = specAny.agentCoreGateways?.length + ? { + agentCoreGateways: specAny.agentCoreGateways, + mcpRuntimeTools: specAny.mcpRuntimeTools, + unassignedTargets: specAny.unassignedTargets, + } + : undefined; // Read deployed state for credential ARNs (populated by pre-deploy identity setup) let deployedState: Record | undefined; diff --git a/src/cli/commands/add/__tests__/add-gateway-target.test.ts b/src/cli/commands/add/__tests__/add-gateway-target.test.ts index f0bbc2614..3f651a16a 100644 --- a/src/cli/commands/add/__tests__/add-gateway-target.test.ts +++ b/src/cli/commands/add/__tests__/add-gateway-target.test.ts @@ -78,9 +78,9 @@ describe('add gateway-target command', () => { const json = JSON.parse(result.stdout); expect(json.success).toBe(true); - // Verify in mcp.json - const mcpSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/mcp.json'), 'utf-8')); - const gateway = mcpSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName); + // Verify in agentcore.json + const projectSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/agentcore.json'), 'utf-8')); + const gateway = projectSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName); const target = gateway?.targets?.find((t: { name: string }) => t.name === targetName); expect(target, 'Target should be in gateway targets').toBeTruthy(); }); @@ -122,8 +122,8 @@ describe('add gateway-target command', () => { expect(json.success).toBe(true); expect(json.toolName).toBe(targetName); - const mcpSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/mcp.json'), 'utf-8')); - const gateway = mcpSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName); + const projectSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/agentcore.json'), 'utf-8')); + const gateway = projectSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName); const target = gateway?.targets?.find((t: { name: string }) => t.name === targetName); expect(target).toBeTruthy(); expect(target.targetType).toBe('lambdaFunctionArn'); diff --git a/src/cli/commands/add/__tests__/add-gateway.test.ts b/src/cli/commands/add/__tests__/add-gateway.test.ts index 50d0f784c..55d677b6b 100644 --- a/src/cli/commands/add/__tests__/add-gateway.test.ts +++ b/src/cli/commands/add/__tests__/add-gateway.test.ts @@ -37,10 +37,10 @@ describe('add gateway command', () => { expect(json.success).toBe(true); expect(json.gatewayName).toBe(gatewayName); - // Verify gateway in mcp.json - const mcpSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/mcp.json'), 'utf-8')); - const gateway = mcpSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName); - expect(gateway, 'Gateway should be in mcp.json').toBeTruthy(); + // Verify gateway in agentcore.json + const projectSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/agentcore.json'), 'utf-8')); + const gateway = projectSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName); + expect(gateway, 'Gateway should be in agentcore.json').toBeTruthy(); expect(gateway.authorizerType).toBe('NONE'); }); @@ -105,10 +105,10 @@ describe('add gateway command', () => { const json = JSON.parse(result.stdout); expect(json.success).toBe(true); - // Verify JWT config in mcp.json - const mcpSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/mcp.json'), 'utf-8')); - const gateway = mcpSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName); - expect(gateway, 'Gateway should be in mcp.json').toBeTruthy(); + // Verify JWT config in agentcore.json + const projectSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/agentcore.json'), 'utf-8')); + const gateway = projectSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName); + expect(gateway, 'Gateway should be in agentcore.json').toBeTruthy(); expect(gateway.authorizerType).toBe('CUSTOM_JWT'); expect(gateway.authorizerConfiguration?.customJwtAuthorizer, 'Should have JWT config').toBeTruthy(); }); @@ -180,15 +180,14 @@ describe('add gateway command', () => { const json = JSON.parse(result.stdout); expect(json.success).toBe(true); - // Verify allowedScopes in mcp.json - const mcpSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/mcp.json'), 'utf-8')); - const gateway = mcpSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName); - expect(gateway, 'Gateway should be in mcp.json').toBeTruthy(); + // Verify allowedScopes in agentcore.json + const projectSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/agentcore.json'), 'utf-8')); + const gateway = projectSpec.agentCoreGateways.find((g: { name: string }) => g.name === gatewayName); + expect(gateway, 'Gateway should be in agentcore.json').toBeTruthy(); expect(gateway.authorizerType).toBe('CUSTOM_JWT'); expect(gateway.authorizerConfiguration?.customJwtAuthorizer?.allowedScopes).toEqual(['scope1', 'scope2']); // Verify managed OAuth credential in agentcore.json - const projectSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/agentcore.json'), 'utf-8')); const credential = projectSpec.credentials.find((c: { name: string }) => c.name === `${gatewayName}-oauth`); expect(credential, 'Managed OAuth credential should exist').toBeTruthy(); expect(credential.type).toBe('OAuthCredentialProvider'); diff --git a/src/cli/commands/add/__tests__/validate.test.ts b/src/cli/commands/add/__tests__/validate.test.ts index 52006aa68..e8ad4365f 100644 --- a/src/cli/commands/add/__tests__/validate.test.ts +++ b/src/cli/commands/add/__tests__/validate.test.ts @@ -17,13 +17,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; const mockReadProjectSpec = vi.fn(); const mockConfigExists = vi.fn().mockReturnValue(true); -const mockReadMcpSpec = vi.fn(); vi.mock('../../../../lib/index.js', () => ({ ConfigIO: class { readProjectSpec = mockReadProjectSpec; configExists = mockConfigExists; - readMcpSpec = mockReadMcpSpec; }, findConfigRoot: vi.fn().mockReturnValue('/mock/project/agentcore'), })); @@ -313,7 +311,7 @@ describe('validate', () => { describe('validateAddGatewayTargetOptions', () => { beforeEach(() => { // By default, mock that the gateway from validGatewayTargetOptions exists - mockReadMcpSpec.mockResolvedValue({ agentCoreGateways: [{ name: 'my-gateway' }] }); + mockReadProjectSpec.mockResolvedValue({ agentCoreGateways: [{ name: 'my-gateway' }] }); }); // AC15: Required fields validated @@ -332,7 +330,7 @@ describe('validate', () => { }); it('returns error when no gateways exist', async () => { - mockReadMcpSpec.mockResolvedValue({ agentCoreGateways: [] }); + mockReadProjectSpec.mockResolvedValue({ agentCoreGateways: [] }); const result = await validateAddGatewayTargetOptions({ ...validGatewayTargetOptions }); expect(result.valid).toBe(false); expect(result.error).toContain('No gateways found'); @@ -340,7 +338,7 @@ describe('validate', () => { }); it('returns error when specified gateway does not exist', async () => { - mockReadMcpSpec.mockResolvedValue({ agentCoreGateways: [{ name: 'other-gateway' }] }); + mockReadProjectSpec.mockResolvedValue({ agentCoreGateways: [{ name: 'other-gateway' }] }); const result = await validateAddGatewayTargetOptions({ ...validGatewayTargetOptions }); expect(result.valid).toBe(false); expect(result.error).toContain('Gateway "my-gateway" not found'); @@ -446,6 +444,7 @@ describe('validate', () => { // AC21: credential validation through outbound auth it('returns error when credential not found', async () => { mockReadProjectSpec.mockResolvedValue({ + agentCoreGateways: [{ name: 'my-gateway' }], credentials: [{ name: 'existing-cred', type: 'ApiKey' }], }); @@ -464,6 +463,7 @@ describe('validate', () => { it('returns error when no credentials configured', async () => { mockReadProjectSpec.mockResolvedValue({ + agentCoreGateways: [{ name: 'my-gateway' }], credentials: [], }); @@ -482,6 +482,7 @@ describe('validate', () => { it('passes when credential exists', async () => { mockReadProjectSpec.mockResolvedValue({ + agentCoreGateways: [{ name: 'my-gateway' }], credentials: [{ name: 'valid-cred', type: 'ApiKey' }], }); diff --git a/src/cli/commands/add/validate.ts b/src/cli/commands/add/validate.ts index 46de1c7a6..96665fe6f 100644 --- a/src/cli/commands/add/validate.ts +++ b/src/cli/commands/add/validate.ts @@ -335,10 +335,8 @@ export async function validateAddGatewayTargetOptions(options: AddGatewayTargetO const gatewayConfigIO = new ConfigIO(); let existingGateways: string[] = []; try { - if (gatewayConfigIO.configExists('mcp')) { - const mcpSpec = await gatewayConfigIO.readMcpSpec(); - existingGateways = mcpSpec.agentCoreGateways.map(g => g.name); - } + const project = await gatewayConfigIO.readProjectSpec(); + existingGateways = project.agentCoreGateways.map(g => g.name); } catch { // If we can't read the config, treat as no gateways } diff --git a/src/cli/commands/create/action.ts b/src/cli/commands/create/action.ts index e19a2ace8..40886a7d7 100644 --- a/src/cli/commands/create/action.ts +++ b/src/cli/commands/create/action.ts @@ -32,6 +32,7 @@ function createDefaultProjectSpec(projectName: string): AgentCoreProjectSpec { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; } diff --git a/src/cli/commands/deploy/actions.ts b/src/cli/commands/deploy/actions.ts index d263e527c..246cc0f88 100644 --- a/src/cli/commands/deploy/actions.ts +++ b/src/cli/commands/deploy/actions.ts @@ -1,5 +1,5 @@ import { ConfigIO, SecureCredentials } from '../../../lib'; -import type { DeployedState } from '../../../schema'; +import type { AgentCoreMcpSpec, DeployedState } from '../../../schema'; import { validateAwsCredentials } from '../../aws/account'; import { createSwitchableIoHost } from '../../cdk/toolkit-lib'; import { @@ -81,13 +81,13 @@ export async function handleDeploy(options: ValidatedDeployOptions): Promise | null = null; try { - mcpSpec = await configIO.readMcpSpec(); + const projectSpec = await configIO.readProjectSpec(); + mcpSpec = { agentCoreGateways: projectSpec.agentCoreGateways }; } catch { - // No mcp.json or invalid — no gateways - mcpSpec = null; + // Project read failed — no gateways } // Preflight: validate project diff --git a/src/cli/commands/logs/__tests__/action.test.ts b/src/cli/commands/logs/__tests__/action.test.ts index ee849c472..a68dea92e 100644 --- a/src/cli/commands/logs/__tests__/action.test.ts +++ b/src/cli/commands/logs/__tests__/action.test.ts @@ -58,6 +58,7 @@ describe('resolveAgentContext', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }, deployedState: { @@ -119,6 +120,7 @@ describe('resolveAgentContext', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }, }); @@ -160,6 +162,7 @@ describe('resolveAgentContext', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }, deployedState: { @@ -209,6 +212,7 @@ describe('resolveAgentContext', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }, }); diff --git a/src/cli/commands/remove/__tests__/remove-all.test.ts b/src/cli/commands/remove/__tests__/remove-all.test.ts index 1693ce6ff..8a59e8c2d 100644 --- a/src/cli/commands/remove/__tests__/remove-all.test.ts +++ b/src/cli/commands/remove/__tests__/remove-all.test.ts @@ -106,21 +106,18 @@ describe('remove all command', () => { expect(json.note).toContain('agentcore deploy'); }); - it('clears gateways from mcp.json after remove all', async () => { - // Write mcp.json with a gateway and target - const mcpPath = join(projectDir, 'agentcore', 'mcp.json'); - await writeFile( - mcpPath, - JSON.stringify({ - agentCoreGateways: [ - { - name: 'TestGateway', - authorizerType: 'NONE', - targets: [{ name: 'test-target', targetType: 'mcpServer', endpoint: 'https://example.com/mcp' }], - }, - ], - }) - ); + it('clears gateways from agentcore.json after remove all', async () => { + // Read current agentcore.json and add a gateway + const projectSpecPath = join(projectDir, 'agentcore', 'agentcore.json'); + const projectSpec = JSON.parse(await readFile(projectSpecPath, 'utf-8')); + projectSpec.agentCoreGateways = [ + { + name: 'TestGateway', + authorizerType: 'NONE', + targets: [{ name: 'test-target', targetType: 'mcpServer', endpoint: 'https://example.com/mcp' }], + }, + ]; + await writeFile(projectSpecPath, JSON.stringify(projectSpec, null, 2)); // Run remove all const result = await runCLI(['remove', 'all', '--force', '--json'], projectDir); @@ -128,8 +125,8 @@ describe('remove all command', () => { const json = JSON.parse(result.stdout); expect(json.success).toBe(true); - // Verify mcp.json gateways are cleared - const mcpAfter = JSON.parse(await readFile(mcpPath, 'utf-8')); - expect(mcpAfter.agentCoreGateways.length, 'Gateways should be cleared after remove all').toBe(0); + // Verify agentcore.json gateways are cleared + const projectSpecAfter = JSON.parse(await readFile(projectSpecPath, 'utf-8')); + expect(projectSpecAfter.agentCoreGateways.length, 'Gateways should be cleared after remove all').toBe(0); }); }); diff --git a/src/cli/commands/remove/__tests__/remove-gateway-target.test.ts b/src/cli/commands/remove/__tests__/remove-gateway-target.test.ts index 4269de872..21a6faa98 100644 --- a/src/cli/commands/remove/__tests__/remove-gateway-target.test.ts +++ b/src/cli/commands/remove/__tests__/remove-gateway-target.test.ts @@ -77,8 +77,8 @@ describe('remove gateway-target command', () => { expect(json.success).toBe(true); // Verify tool is removed from gateway targets - const mcpSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/mcp.json'), 'utf-8')); - const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === tempGateway); + const projectSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/agentcore.json'), 'utf-8')); + const gateway = projectSpec.agentCoreGateways?.find((g: { name: string }) => g.name === tempGateway); const target = gateway?.targets?.find((t: { name: string }) => t.name === tempTool); expect(!target, 'Tool should be removed from gateway targets').toBeTruthy(); }); diff --git a/src/cli/commands/remove/__tests__/remove-gateway.test.ts b/src/cli/commands/remove/__tests__/remove-gateway.test.ts index b2dfd1f62..cd21f3e56 100644 --- a/src/cli/commands/remove/__tests__/remove-gateway.test.ts +++ b/src/cli/commands/remove/__tests__/remove-gateway.test.ts @@ -87,8 +87,8 @@ describe('remove gateway command', () => { expect(json.success).toBe(true); // Verify gateway is removed - const mcpSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/mcp.json'), 'utf-8')); - const gateway = mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === tempGateway); + const projectSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/agentcore.json'), 'utf-8')); + const gateway = projectSpec.agentCoreGateways?.find((g: { name: string }) => g.name === tempGateway); expect(!gateway, 'Gateway should be removed').toBeTruthy(); }); @@ -122,9 +122,9 @@ describe('remove gateway command', () => { const json = JSON.parse(result.stdout); expect(json.success).toBe(true); - // Verify gateway is removed from mcp.json - const mcpSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/mcp.json'), 'utf-8')); - expect(mcpSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName)).toBeUndefined(); + // Verify gateway is removed from agentcore.json + const projectSpec = JSON.parse(await readFile(join(projectDir, 'agentcore/agentcore.json'), 'utf-8')); + expect(projectSpec.agentCoreGateways?.find((g: { name: string }) => g.name === gatewayName)).toBeUndefined(); }); }); }); diff --git a/src/cli/commands/remove/command.tsx b/src/cli/commands/remove/command.tsx index 408c407a4..fe508cb86 100644 --- a/src/cli/commands/remove/command.tsx +++ b/src/cli/commands/remove/command.tsx @@ -22,7 +22,7 @@ async function handleRemoveAll(_options: RemoveAllOptions): Promise { expect(result.every(r => r.deploymentState === 'local-only')).toBe(true); }); - it('marks gateway as deployed when in both local mcp spec and deployed state', () => { - const mcpSpec = { + it('marks gateway as deployed when in both local project and deployed state', () => { + const project = { + ...baseProject, agentCoreGateways: [{ name: 'my-gateway', targets: [{ name: 't1' }, { name: 't2' }] }], - } as unknown as AgentCoreMcpSpec; + } as unknown as AgentCoreProjectSpec; const resources: DeployedResourceState = { mcp: { @@ -239,7 +240,7 @@ describe('computeResourceStatuses', () => { }, }; - const result = computeResourceStatuses(baseProject, resources, mcpSpec); + const result = computeResourceStatuses(project, resources); const gwEntry = result.find(r => r.resourceType === 'gateway' && r.name === 'my-gateway'); expect(gwEntry).toBeDefined(); @@ -249,11 +250,12 @@ describe('computeResourceStatuses', () => { }); it('marks gateway as local-only when not in deployed state', () => { - const mcpSpec = { + const project = { + ...baseProject, agentCoreGateways: [{ name: 'my-gateway', targets: [{ name: 't1' }] }], - } as unknown as AgentCoreMcpSpec; + } as unknown as AgentCoreProjectSpec; - const result = computeResourceStatuses(baseProject, undefined, mcpSpec); + const result = computeResourceStatuses(project, undefined); const gwEntry = result.find(r => r.resourceType === 'gateway' && r.name === 'my-gateway'); expect(gwEntry).toBeDefined(); @@ -261,11 +263,7 @@ describe('computeResourceStatuses', () => { expect(gwEntry!.detail).toBe('1 target'); }); - it('marks gateway as pending-removal when in deployed state but not in local mcp spec', () => { - const mcpSpec = { - agentCoreGateways: [], - } as unknown as AgentCoreMcpSpec; - + it('marks gateway as pending-removal when in deployed state but not in local project', () => { const resources: DeployedResourceState = { mcp: { gateways: { @@ -277,7 +275,7 @@ describe('computeResourceStatuses', () => { }, }; - const result = computeResourceStatuses(baseProject, resources, mcpSpec); + const result = computeResourceStatuses(baseProject, resources); const gwEntry = result.find(r => r.resourceType === 'gateway' && r.name === 'removed-gateway'); expect(gwEntry).toBeDefined(); diff --git a/src/cli/commands/status/action.ts b/src/cli/commands/status/action.ts index 2bc433f90..80c1f7cc2 100644 --- a/src/cli/commands/status/action.ts +++ b/src/cli/commands/status/action.ts @@ -1,11 +1,5 @@ import { ConfigIO } from '../../../lib'; -import type { - AgentCoreMcpSpec, - AgentCoreProjectSpec, - AwsDeploymentTargets, - DeployedResourceState, - DeployedState, -} from '../../../schema'; +import type { AgentCoreProjectSpec, AwsDeploymentTargets, DeployedResourceState, DeployedState } from '../../../schema'; import { getAgentRuntimeStatus } from '../../aws'; import { getEvaluator, getOnlineEvaluationConfig } from '../../aws/agentcore-control'; import { getErrorMessage } from '../../errors'; @@ -45,7 +39,6 @@ export interface StatusContext { project: AgentCoreProjectSpec; deployedState: DeployedState; awsTargets: AwsDeploymentTargets; - mcpSpec?: AgentCoreMcpSpec; } export interface RuntimeLookupResult { @@ -62,16 +55,15 @@ export interface RuntimeLookupResult { * Gracefully handles missing deployed-state by returning empty targets. */ export async function loadStatusConfig(configIO: ConfigIO = new ConfigIO()): Promise { - const [project, awsTargets, deployedState, mcpSpec] = await Promise.all([ + const [project, awsTargets, deployedState] = await Promise.all([ configIO.readProjectSpec(), configIO.readAWSDeploymentTargets(), configIO.configExists('state') ? configIO.readDeployedState() : (Promise.resolve({ targets: {} }) as Promise), - configIO.configExists('mcp') ? configIO.readMcpSpec() : Promise.resolve(undefined), ]); - return { project, deployedState, awsTargets, mcpSpec }; + return { project, deployedState, awsTargets }; } /** @@ -124,8 +116,7 @@ function diffResourceSet({ export function computeResourceStatuses( project: AgentCoreProjectSpec, - resources: DeployedResourceState | undefined, - mcpSpec?: AgentCoreMcpSpec + resources: DeployedResourceState | undefined ): ResourceStatusEntry[] { const agents = diffResourceSet({ resourceType: 'agent', @@ -155,7 +146,7 @@ export function computeResourceStatuses( const gateways = diffResourceSet({ resourceType: 'gateway', - localItems: mcpSpec?.agentCoreGateways ?? [], + localItems: project.agentCoreGateways ?? [], deployedRecord: resources?.mcp?.gateways ?? {}, getIdentifier: deployed => deployed.gatewayId, getLocalDetail: item => { @@ -226,7 +217,7 @@ export async function handleProjectStatus( options: { targetName?: string } = {} ): Promise { const logger = new ExecLogger({ command: 'status' }); - const { project, deployedState, awsTargets, mcpSpec } = context; + const { project, deployedState, awsTargets } = context; logger.startStep('Resolve target'); const deployedTargetNames = Object.keys(deployedState.targets); @@ -259,7 +250,7 @@ export async function handleProjectStatus( const targetConfig = selectedTargetName ? awsTargets.find(t => t.name === selectedTargetName) : undefined; const targetResources = selectedTargetName ? deployedState.targets[selectedTargetName]?.resources : undefined; - const resources = computeResourceStatuses(project, targetResources, mcpSpec); + const resources = computeResourceStatuses(project, targetResources); const deployed = resources.filter(r => r.deploymentState === 'deployed').length; const localOnly = resources.filter(r => r.deploymentState === 'local-only').length; diff --git a/src/cli/external-requirements/__tests__/checks-extended.test.ts b/src/cli/external-requirements/__tests__/checks-extended.test.ts index 918a1a5d4..63aa3d4d9 100644 --- a/src/cli/external-requirements/__tests__/checks-extended.test.ts +++ b/src/cli/external-requirements/__tests__/checks-extended.test.ts @@ -51,6 +51,7 @@ describe('requiresUv', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; expect(requiresUv(project)).toBe(true); @@ -75,6 +76,7 @@ describe('requiresUv', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; expect(requiresUv(project)).toBe(false); @@ -89,6 +91,7 @@ describe('requiresUv', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; expect(requiresUv(project)).toBe(false); @@ -115,6 +118,7 @@ describe('requiresContainerRuntime', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; expect(requiresContainerRuntime(project)).toBe(true); @@ -139,6 +143,7 @@ describe('requiresContainerRuntime', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; expect(requiresContainerRuntime(project)).toBe(false); @@ -153,6 +158,7 @@ describe('requiresContainerRuntime', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; expect(requiresContainerRuntime(project)).toBe(false); @@ -186,6 +192,7 @@ describe('requiresContainerRuntime', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; expect(requiresContainerRuntime(project)).toBe(true); @@ -251,6 +258,7 @@ describe('checkDependencyVersions', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; @@ -269,6 +277,7 @@ describe('checkDependencyVersions', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; @@ -296,6 +305,7 @@ describe('checkDependencyVersions', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; diff --git a/src/cli/operations/agent/generate/schema-mapper.ts b/src/cli/operations/agent/generate/schema-mapper.ts index fee8dd339..552e69221 100644 --- a/src/cli/operations/agent/generate/schema-mapper.ts +++ b/src/cli/operations/agent/generate/schema-mapper.ts @@ -200,13 +200,9 @@ export function mapModelProviderToIdentityProviders( async function mapGatewaysToGatewayProviders(): Promise { try { const configIO = new ConfigIO(); - if (!configIO.configExists('mcp')) { - return []; - } - const mcpSpec = await configIO.readMcpSpec(); const project = await configIO.readProjectSpec(); - return mcpSpec.agentCoreGateways.map(gateway => { + return project.agentCoreGateways.map(gateway => { const config: GatewayProviderRenderConfig = { name: gateway.name, envVarName: GatewayPrimitive.computeDefaultGatewayEnvVarName(gateway.name), diff --git a/src/cli/operations/agent/generate/write-agent-to-project.ts b/src/cli/operations/agent/generate/write-agent-to-project.ts index a3561d22d..0da650def 100644 --- a/src/cli/operations/agent/generate/write-agent-to-project.ts +++ b/src/cli/operations/agent/generate/write-agent-to-project.ts @@ -69,6 +69,7 @@ export async function writeAgentToProject(config: GenerateConfig, options?: Writ credentials, evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; diff --git a/src/cli/operations/attach.ts b/src/cli/operations/attach.ts index ff8e0791b..f2707cb55 100644 --- a/src/cli/operations/attach.ts +++ b/src/cli/operations/attach.ts @@ -45,28 +45,27 @@ export async function getCredentials(): Promise { } /** - * Get list of MCP runtime tools from mcp.json. + * Get list of MCP runtime tools from agentcore.json. */ export async function getMcpRuntimeTools(): Promise { try { const configIO = new ConfigIO(); - const mcpSpec = await configIO.readMcpSpec(); - if (!mcpSpec?.mcpRuntimeTools) return []; - return mcpSpec.mcpRuntimeTools.map(tool => tool.name); + const project = await configIO.readProjectSpec(); + if (!project?.mcpRuntimeTools) return []; + return project.mcpRuntimeTools.map(tool => tool.name); } catch { return []; } } /** - * Get list of gateways from mcp.json. + * Get list of gateways from agentcore.json. */ export async function getGateways(): Promise { try { const configIO = new ConfigIO(); - const mcpSpec = await configIO.readMcpSpec(); - if (!mcpSpec?.agentCoreGateways) return []; - return mcpSpec.agentCoreGateways.map(gw => gw.name); + const project = await configIO.readProjectSpec(); + return project.agentCoreGateways.map(gw => gw.name); } catch { return []; } @@ -83,7 +82,7 @@ export interface BindMcpRuntimeConfig { /** * Bind an agent to an MCP runtime tool. - * Adds the binding to the MCP runtime's bindings array in mcp.json. + * Adds the binding to the MCP runtime's bindings array in agentcore.json. */ export async function bindMcpRuntimeToAgent(mcpRuntimeName: string, config: BindMcpRuntimeConfig): Promise { const configIO = new ConfigIO(); @@ -96,10 +95,9 @@ export async function bindMcpRuntimeToAgent(mcpRuntimeName: string, config: Bind } // Find the MCP runtime tool - const mcpSpec = await configIO.readMcpSpec(); - const runtimeTool = mcpSpec.mcpRuntimeTools?.find(t => t.name === mcpRuntimeName); + const runtimeTool = project.mcpRuntimeTools?.find(t => t.name === mcpRuntimeName); if (!runtimeTool) { - throw new Error(`MCP runtime tool "${mcpRuntimeName}" not found in mcp.json.`); + throw new Error(`MCP runtime tool "${mcpRuntimeName}" not found in agentcore.json.`); } // Initialize bindings array if needed @@ -116,5 +114,5 @@ export async function bindMcpRuntimeToAgent(mcpRuntimeName: string, config: Bind }; runtimeTool.bindings.push(binding); - await configIO.writeMcpSpec(mcpSpec); + await configIO.writeProjectSpec(project); } diff --git a/src/cli/operations/deploy/__tests__/preflight.test.ts b/src/cli/operations/deploy/__tests__/preflight.test.ts index 0818acf7c..0f7da61c6 100644 --- a/src/cli/operations/deploy/__tests__/preflight.test.ts +++ b/src/cli/operations/deploy/__tests__/preflight.test.ts @@ -1,14 +1,14 @@ import { formatError, validateProject } from '../preflight.js'; import { afterEach, describe, expect, it, vi } from 'vitest'; -const { mockReadProjectSpec, mockReadAWSDeploymentTargets, mockReadMcpSpec, mockReadDeployedState, mockConfigExists } = - vi.hoisted(() => ({ +const { mockReadProjectSpec, mockReadAWSDeploymentTargets, mockReadDeployedState, mockConfigExists } = vi.hoisted( + () => ({ mockReadProjectSpec: vi.fn(), mockReadAWSDeploymentTargets: vi.fn(), - mockReadMcpSpec: vi.fn(), mockReadDeployedState: vi.fn(), mockConfigExists: vi.fn(), - })); + }) +); const { mockValidate } = vi.hoisted(() => ({ mockValidate: vi.fn(), @@ -29,7 +29,6 @@ vi.mock('../../../../lib/index.js', () => ({ } readProjectSpec = mockReadProjectSpec; readAWSDeploymentTargets = mockReadAWSDeploymentTargets; - readMcpSpec = mockReadMcpSpec; readDeployedState = mockReadDeployedState; configExists = mockConfigExists; }, @@ -55,12 +54,9 @@ describe('validateProject', () => { mockReadProjectSpec.mockResolvedValue({ name: 'test-project', agents: [], - }); - mockReadAWSDeploymentTargets.mockResolvedValue([]); - mockConfigExists.mockReturnValue(true); - mockReadMcpSpec.mockResolvedValue({ agentCoreGateways: [{ name: 'test-gateway' }], }); + mockReadAWSDeploymentTargets.mockResolvedValue([]); mockValidateAwsCredentials.mockResolvedValue(undefined); const result = await validateProject(); @@ -75,9 +71,9 @@ describe('validateProject', () => { mockReadProjectSpec.mockResolvedValue({ name: 'test-project', agents: [], + agentCoreGateways: [], }); mockReadAWSDeploymentTargets.mockResolvedValue([]); - mockReadMcpSpec.mockRejectedValue(new Error('No mcp.json')); mockReadDeployedState.mockRejectedValue(new Error('No deployed state')); await expect(validateProject()).rejects.toThrow( @@ -92,9 +88,9 @@ describe('validateProject', () => { name: 'test-project', agents: [], memories: [{ name: 'test-memory', strategies: [] }], + agentCoreGateways: [], }); mockReadAWSDeploymentTargets.mockResolvedValue([]); - mockReadMcpSpec.mockRejectedValue(new Error('No mcp.json')); mockValidateAwsCredentials.mockResolvedValue(undefined); const result = await validateProject(); @@ -109,12 +105,9 @@ describe('validateProject', () => { mockReadProjectSpec.mockResolvedValue({ name: 'test-project', agents: [{ name: 'test-agent' }], - }); - mockReadAWSDeploymentTargets.mockResolvedValue([]); - mockConfigExists.mockReturnValue(true); - mockReadMcpSpec.mockResolvedValue({ agentCoreGateways: [{ name: 'test-gateway' }], }); + mockReadAWSDeploymentTargets.mockResolvedValue([]); mockValidateAwsCredentials.mockResolvedValue(undefined); const result = await validateProject(); diff --git a/src/cli/operations/deploy/preflight.ts b/src/cli/operations/deploy/preflight.ts index 19923488d..c9de9a900 100644 --- a/src/cli/operations/deploy/preflight.ts +++ b/src/cli/operations/deploy/preflight.ts @@ -85,14 +85,8 @@ export async function validateProject(): Promise { const hasEvaluators = projectSpec.evaluators && projectSpec.evaluators.length > 0; const hasPolicyEngines = projectSpec.policyEngines && projectSpec.policyEngines.length > 0; - // Check for gateways in mcp.json - let hasGateways = false; - try { - const mcpSpec = await configIO.readMcpSpec(); - hasGateways = mcpSpec.agentCoreGateways && mcpSpec.agentCoreGateways.length > 0; - } catch { - // No mcp.json or invalid — no gateways - } + // Check for gateways in agentcore.json + const hasGateways = projectSpec.agentCoreGateways && projectSpec.agentCoreGateways.length > 0; if (!hasAgents && !hasGateways && !hasMemories && !hasEvaluators && !hasPolicyEngines) { let hasExistingStack = false; diff --git a/src/cli/operations/dev/__tests__/config.test.ts b/src/cli/operations/dev/__tests__/config.test.ts index 9b14afcae..ca54987da 100644 --- a/src/cli/operations/dev/__tests__/config.test.ts +++ b/src/cli/operations/dev/__tests__/config.test.ts @@ -18,6 +18,7 @@ describe('getDevConfig', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; @@ -44,6 +45,7 @@ describe('getDevConfig', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; @@ -70,6 +72,7 @@ describe('getDevConfig', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; @@ -102,6 +105,7 @@ describe('getDevConfig', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; @@ -129,6 +133,7 @@ describe('getDevConfig', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; @@ -154,6 +159,7 @@ describe('getDevConfig', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; @@ -182,6 +188,7 @@ describe('getDevConfig', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; @@ -210,6 +217,7 @@ describe('getDevConfig', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; @@ -238,6 +246,7 @@ describe('getDevConfig', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; @@ -265,6 +274,7 @@ describe('getDevConfig', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; @@ -292,6 +302,7 @@ describe('getDevConfig', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; @@ -319,6 +330,7 @@ describe('getDevConfig', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; @@ -346,6 +358,7 @@ describe('getDevConfig', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; @@ -388,6 +401,7 @@ describe('getAgentPort', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; @@ -404,6 +418,7 @@ describe('getAgentPort', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; @@ -425,6 +440,7 @@ describe('getDevSupportedAgents', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; @@ -450,6 +466,7 @@ describe('getDevSupportedAgents', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; @@ -484,6 +501,7 @@ describe('getDevSupportedAgents', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; @@ -511,6 +529,7 @@ describe('getDevSupportedAgents', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; @@ -547,6 +566,7 @@ describe('getDevSupportedAgents', () => { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; diff --git a/src/cli/operations/dev/__tests__/gateway-env.test.ts b/src/cli/operations/dev/__tests__/gateway-env.test.ts index d880b356f..b295c747b 100644 --- a/src/cli/operations/dev/__tests__/gateway-env.test.ts +++ b/src/cli/operations/dev/__tests__/gateway-env.test.ts @@ -1,15 +1,15 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -const { mockReadDeployedState, mockReadMcpSpec, mockConfigExists } = vi.hoisted(() => ({ +const { mockReadDeployedState, mockReadProjectSpec, mockConfigExists } = vi.hoisted(() => ({ mockReadDeployedState: vi.fn(), - mockReadMcpSpec: vi.fn(), + mockReadProjectSpec: vi.fn(), mockConfigExists: vi.fn(), })); vi.mock('../../../../lib/index.js', () => ({ ConfigIO: class { readDeployedState = mockReadDeployedState; - readMcpSpec = mockReadMcpSpec; + readProjectSpec = mockReadProjectSpec; configExists = mockConfigExists; }, })); @@ -49,7 +49,7 @@ describe('getGatewayEnvVars', () => { }, }); mockConfigExists.mockReturnValue(true); - mockReadMcpSpec.mockResolvedValue({ + mockReadProjectSpec.mockResolvedValue({ agentCoreGateways: [{ name: 'my-gateway', authorizerType: 'CUSTOM_JWT' }], }); @@ -69,7 +69,7 @@ describe('getGatewayEnvVars', () => { }, }); mockConfigExists.mockReturnValue(true); - mockReadMcpSpec.mockResolvedValue({ agentCoreGateways: [] }); + mockReadProjectSpec.mockResolvedValue({ agentCoreGateways: [] }); const result = await getGatewayEnvVars(); expect(result.AGENTCORE_GATEWAY_TEST_GW_AUTH_TYPE).toBe('NONE'); diff --git a/src/cli/operations/dev/gateway-env.ts b/src/cli/operations/dev/gateway-env.ts index 78d43bcf2..3cfe266a6 100644 --- a/src/cli/operations/dev/gateway-env.ts +++ b/src/cli/operations/dev/gateway-env.ts @@ -6,7 +6,7 @@ export async function getGatewayEnvVars(): Promise> { try { const deployedState = await configIO.readDeployedState(); - const mcpSpec = configIO.configExists('mcp') ? await configIO.readMcpSpec() : undefined; + const project = await configIO.readProjectSpec(); // Iterate all targets (not just 'default') for (const target of Object.values(deployedState?.targets ?? {})) { @@ -17,13 +17,13 @@ export async function getGatewayEnvVars(): Promise> { const sanitized = name.toUpperCase().replace(/-/g, '_'); envVars[`AGENTCORE_GATEWAY_${sanitized}_URL`] = gateway.gatewayUrl; - const gatewaySpec = mcpSpec?.agentCoreGateways?.find(g => g.name === name); + const gatewaySpec = project.agentCoreGateways?.find(g => g.name === name); const authType = gatewaySpec?.authorizerType ?? 'NONE'; envVars[`AGENTCORE_GATEWAY_${sanitized}_AUTH_TYPE`] = authType; } } } catch { - // No deployed state or mcp.json — skip gateway env vars + // No deployed state or project spec issue — skip gateway env vars } return envVars; diff --git a/src/cli/operations/mcp/__tests__/create-mcp-utils.test.ts b/src/cli/operations/mcp/__tests__/create-mcp-utils.test.ts index 5907ab34b..c6c32e1e7 100644 --- a/src/cli/operations/mcp/__tests__/create-mcp-utils.test.ts +++ b/src/cli/operations/mcp/__tests__/create-mcp-utils.test.ts @@ -2,18 +2,16 @@ import { GatewayPrimitive } from '../../../primitives/GatewayPrimitive.js'; import { GatewayTargetPrimitive } from '../../../primitives/GatewayTargetPrimitive.js'; import { afterEach, describe, expect, it, vi } from 'vitest'; -const { mockReadMcpSpec, mockWriteMcpSpec, mockReadProjectSpec, mockConfigExists } = vi.hoisted(() => ({ - mockReadMcpSpec: vi.fn(), - mockWriteMcpSpec: vi.fn(), +const { mockReadProjectSpec, mockWriteProjectSpec, mockConfigExists } = vi.hoisted(() => ({ mockReadProjectSpec: vi.fn(), + mockWriteProjectSpec: vi.fn(), mockConfigExists: vi.fn(), })); vi.mock('../../../../lib/index.js', () => ({ ConfigIO: class { - readMcpSpec = mockReadMcpSpec; - writeMcpSpec = mockWriteMcpSpec; readProjectSpec = mockReadProjectSpec; + writeProjectSpec = mockWriteProjectSpec; configExists = mockConfigExists; }, requireConfigRoot: () => '/project/agentcore', @@ -40,17 +38,16 @@ describe('getExistingGateways', () => { afterEach(() => vi.clearAllMocks()); - it('returns empty array when mcp config does not exist', async () => { - mockConfigExists.mockReturnValue(false); + it('returns empty array when no project exists', async () => { + mockReadProjectSpec.mockRejectedValue(new Error('No project')); const result = await gatewayPrimitive.getExistingGateways(); expect(result).toEqual([]); }); - it('returns gateway names from mcp spec', async () => { - mockConfigExists.mockReturnValue(true); - mockReadMcpSpec.mockResolvedValue({ + it('returns gateway names from project spec', async () => { + mockReadProjectSpec.mockResolvedValue({ agentCoreGateways: [{ name: 'gw-1' }, { name: 'gw-2' }], }); @@ -60,9 +57,7 @@ describe('getExistingGateways', () => { }); it('returns empty array on error', async () => { - mockConfigExists.mockImplementation(() => { - throw new Error('read error'); - }); + mockReadProjectSpec.mockRejectedValue(new Error('read error')); const result = await gatewayPrimitive.getExistingGateways(); @@ -75,8 +70,8 @@ describe('getExistingToolNames', () => { afterEach(() => vi.clearAllMocks()); - it('returns empty array when mcp config does not exist', async () => { - mockConfigExists.mockReturnValue(false); + it('returns empty array when no project exists', async () => { + mockReadProjectSpec.mockRejectedValue(new Error('No project')); const result = await gatewayTargetPrimitive.getExistingToolNames(); @@ -84,8 +79,7 @@ describe('getExistingToolNames', () => { }); it('returns tool names from gateway targets', async () => { - mockConfigExists.mockReturnValue(true); - mockReadMcpSpec.mockResolvedValue({ + mockReadProjectSpec.mockResolvedValue({ agentCoreGateways: [ { name: 'gw-1', @@ -105,8 +99,7 @@ describe('getExistingToolNames', () => { }); it('returns empty array when no gateway targets have tool definitions', async () => { - mockConfigExists.mockReturnValue(true); - mockReadMcpSpec.mockResolvedValue({ + mockReadProjectSpec.mockResolvedValue({ agentCoreGateways: [{ name: 'gw', targets: [] }], }); @@ -116,8 +109,7 @@ describe('getExistingToolNames', () => { }); it('returns empty array on error', async () => { - mockConfigExists.mockReturnValue(true); - mockReadMcpSpec.mockRejectedValue(new Error('corrupt')); + mockReadProjectSpec.mockRejectedValue(new Error('corrupt')); const result = await gatewayTargetPrimitive.getExistingToolNames(); @@ -130,9 +122,18 @@ describe('GatewayPrimitive.add (createGateway)', () => { afterEach(() => vi.clearAllMocks()); - it('creates gateway when mcp config does not exist', async () => { - mockConfigExists.mockReturnValue(false); - mockWriteMcpSpec.mockResolvedValue(undefined); + it('creates gateway when project has no gateways', async () => { + mockReadProjectSpec.mockResolvedValue({ + name: 'test', + version: 1, + agents: [], + memories: [], + credentials: [], + evaluators: [], + onlineEvalConfigs: [], + agentCoreGateways: [], + }); + mockWriteProjectSpec.mockResolvedValue(undefined); const result = await gatewayPrimitive.add({ name: 'new-gw', @@ -141,7 +142,7 @@ describe('GatewayPrimitive.add (createGateway)', () => { }); expect(result).toEqual(expect.objectContaining({ success: true, gatewayName: 'new-gw' })); - expect(mockWriteMcpSpec).toHaveBeenCalledWith( + expect(mockWriteProjectSpec).toHaveBeenCalledWith( expect.objectContaining({ agentCoreGateways: [ expect.objectContaining({ @@ -155,11 +156,17 @@ describe('GatewayPrimitive.add (createGateway)', () => { }); it('appends to existing gateways', async () => { - mockConfigExists.mockReturnValue(true); - mockReadMcpSpec.mockResolvedValue({ + mockReadProjectSpec.mockResolvedValue({ + name: 'test', + version: 1, + agents: [], + memories: [], + credentials: [], + evaluators: [], + onlineEvalConfigs: [], agentCoreGateways: [{ name: 'existing-gw', targets: [] }], }); - mockWriteMcpSpec.mockResolvedValue(undefined); + mockWriteProjectSpec.mockResolvedValue(undefined); const result = await gatewayPrimitive.add({ name: 'new-gw', @@ -168,12 +175,18 @@ describe('GatewayPrimitive.add (createGateway)', () => { }); expect(result).toEqual(expect.objectContaining({ success: true, gatewayName: 'new-gw' })); - expect(mockWriteMcpSpec.mock.calls[0]![0].agentCoreGateways).toHaveLength(2); + expect(mockWriteProjectSpec.mock.calls[0]![0].agentCoreGateways).toHaveLength(2); }); it('returns error when gateway name already exists', async () => { - mockConfigExists.mockReturnValue(true); - mockReadMcpSpec.mockResolvedValue({ + mockReadProjectSpec.mockResolvedValue({ + name: 'test', + version: 1, + agents: [], + memories: [], + credentials: [], + evaluators: [], + onlineEvalConfigs: [], agentCoreGateways: [{ name: 'dup-gw', targets: [] }], }); @@ -189,8 +202,17 @@ describe('GatewayPrimitive.add (createGateway)', () => { }); it('includes JWT authorizer config when CUSTOM_JWT', async () => { - mockConfigExists.mockReturnValue(false); - mockWriteMcpSpec.mockResolvedValue(undefined); + mockReadProjectSpec.mockResolvedValue({ + name: 'test', + version: 1, + agents: [], + memories: [], + credentials: [], + evaluators: [], + onlineEvalConfigs: [], + agentCoreGateways: [], + }); + mockWriteProjectSpec.mockResolvedValue(undefined); await gatewayPrimitive.add({ name: 'jwt-gw', @@ -201,7 +223,7 @@ describe('GatewayPrimitive.add (createGateway)', () => { allowedClients: 'client1', }); - expect(mockWriteMcpSpec.mock.calls[0]![0].agentCoreGateways[0].authorizerConfiguration).toEqual({ + expect(mockWriteProjectSpec.mock.calls[0]![0].agentCoreGateways[0].authorizerConfiguration).toEqual({ customJwtAuthorizer: { discoveryUrl: 'https://example.com/.well-known/openid', allowedAudience: ['aud1'], diff --git a/src/cli/operations/remove/__tests__/remove-gateway-ops.test.ts b/src/cli/operations/remove/__tests__/remove-gateway-ops.test.ts index 86a0bf733..54293df5a 100644 --- a/src/cli/operations/remove/__tests__/remove-gateway-ops.test.ts +++ b/src/cli/operations/remove/__tests__/remove-gateway-ops.test.ts @@ -1,14 +1,14 @@ import { GatewayPrimitive } from '../../../primitives/GatewayPrimitive.js'; import { afterEach, describe, expect, it, vi } from 'vitest'; -const mockReadMcpSpec = vi.fn(); -const mockWriteMcpSpec = vi.fn(); +const mockReadProjectSpec = vi.fn(); +const mockWriteProjectSpec = vi.fn(); const mockConfigExists = vi.fn(); vi.mock('../../../../lib/index.js', () => ({ ConfigIO: class { - readMcpSpec = mockReadMcpSpec; - writeMcpSpec = mockWriteMcpSpec; + readProjectSpec = mockReadProjectSpec; + writeProjectSpec = mockWriteProjectSpec; configExists = mockConfigExists; }, })); @@ -31,22 +31,22 @@ describe('getRemovable', () => { it('returns gateway resources', async () => { mockConfigExists.mockReturnValue(true); - mockReadMcpSpec.mockResolvedValue(makeMcpSpec(['gw1', 'gw2'])); + mockReadProjectSpec.mockResolvedValue(makeMcpSpec(['gw1', 'gw2'])); const result = await primitive.getRemovable(); expect(result).toEqual([{ name: 'gw1' }, { name: 'gw2' }]); }); - it('returns empty when no mcp config', async () => { - mockConfigExists.mockReturnValue(false); + it('returns empty when no project config', async () => { + mockReadProjectSpec.mockRejectedValue(new Error('No project')); expect(await primitive.getRemovable()).toEqual([]); }); it('returns empty on error', async () => { mockConfigExists.mockReturnValue(true); - mockReadMcpSpec.mockRejectedValue(new Error('fail')); + mockReadProjectSpec.mockRejectedValue(new Error('fail')); expect(await primitive.getRemovable()).toEqual([]); }); @@ -56,7 +56,7 @@ describe('previewRemove', () => { afterEach(() => vi.clearAllMocks()); it('returns preview for gateway without targets', async () => { - mockReadMcpSpec.mockResolvedValue(makeMcpSpec(['myGw'])); + mockReadProjectSpec.mockResolvedValue(makeMcpSpec(['myGw'])); const preview = await primitive.previewRemove('myGw'); @@ -65,7 +65,7 @@ describe('previewRemove', () => { }); it('notes orphaned targets when gateway has targets', async () => { - mockReadMcpSpec.mockResolvedValue(makeMcpSpec(['myGw'], 3)); + mockReadProjectSpec.mockResolvedValue(makeMcpSpec(['myGw'], 3)); const preview = await primitive.previewRemove('myGw'); @@ -73,7 +73,7 @@ describe('previewRemove', () => { }); it('throws when gateway not found', async () => { - mockReadMcpSpec.mockResolvedValue(makeMcpSpec(['other'])); + mockReadProjectSpec.mockResolvedValue(makeMcpSpec(['other'])); await expect(primitive.previewRemove('missing')).rejects.toThrow('Gateway "missing" not found'); }); @@ -83,13 +83,13 @@ describe('remove', () => { afterEach(() => vi.clearAllMocks()); it('removes gateway and writes spec', async () => { - mockReadMcpSpec.mockResolvedValue(makeMcpSpec(['gw1', 'gw2'])); - mockWriteMcpSpec.mockResolvedValue(undefined); + mockReadProjectSpec.mockResolvedValue(makeMcpSpec(['gw1', 'gw2'])); + mockWriteProjectSpec.mockResolvedValue(undefined); const result = await primitive.remove('gw1'); expect(result).toEqual({ success: true }); - expect(mockWriteMcpSpec).toHaveBeenCalledWith( + expect(mockWriteProjectSpec).toHaveBeenCalledWith( expect.objectContaining({ agentCoreGateways: [expect.objectContaining({ name: 'gw2' })], }) @@ -97,7 +97,7 @@ describe('remove', () => { }); it('returns error when gateway not found', async () => { - mockReadMcpSpec.mockResolvedValue(makeMcpSpec([])); + mockReadProjectSpec.mockResolvedValue(makeMcpSpec([])); const result = await primitive.remove('missing'); @@ -105,7 +105,7 @@ describe('remove', () => { }); it('returns error on exception', async () => { - mockReadMcpSpec.mockRejectedValue(new Error('read fail')); + mockReadProjectSpec.mockRejectedValue(new Error('read fail')); const result = await primitive.remove('gw1'); diff --git a/src/cli/operations/remove/__tests__/remove-gateway-target.test.ts b/src/cli/operations/remove/__tests__/remove-gateway-target.test.ts index b9b6a9658..0cb0b22cb 100644 --- a/src/cli/operations/remove/__tests__/remove-gateway-target.test.ts +++ b/src/cli/operations/remove/__tests__/remove-gateway-target.test.ts @@ -5,15 +5,21 @@ import { } from '../remove-gateway-target.js'; import { afterEach, describe, expect, it, vi } from 'vitest'; -const { mockReadMcpSpec, mockWriteMcpSpec, mockReadMcpDefs, mockWriteMcpDefs, mockConfigExists, mockGetProjectRoot } = - vi.hoisted(() => ({ - mockReadMcpSpec: vi.fn(), - mockWriteMcpSpec: vi.fn(), - mockReadMcpDefs: vi.fn(), - mockWriteMcpDefs: vi.fn(), - mockConfigExists: vi.fn(), - mockGetProjectRoot: vi.fn(), - })); +const { + mockReadProjectSpec, + mockWriteProjectSpec, + mockReadMcpDefs, + mockWriteMcpDefs, + mockConfigExists, + mockGetProjectRoot, +} = vi.hoisted(() => ({ + mockReadProjectSpec: vi.fn(), + mockWriteProjectSpec: vi.fn(), + mockReadMcpDefs: vi.fn(), + mockWriteMcpDefs: vi.fn(), + mockConfigExists: vi.fn(), + mockGetProjectRoot: vi.fn(), +})); const { mockExistsSync, mockRm } = vi.hoisted(() => ({ mockExistsSync: vi.fn(), @@ -23,8 +29,8 @@ const { mockExistsSync, mockRm } = vi.hoisted(() => ({ vi.mock('../../../../lib/index.js', () => ({ ConfigIO: class { configExists = mockConfigExists; - readMcpSpec = mockReadMcpSpec; - writeMcpSpec = mockWriteMcpSpec; + readProjectSpec = mockReadProjectSpec; + writeProjectSpec = mockWriteProjectSpec; readMcpDefs = mockReadMcpDefs; writeMcpDefs = mockWriteMcpDefs; getProjectRoot = mockGetProjectRoot; @@ -44,7 +50,7 @@ describe('getRemovableGatewayTargets', () => { it('returns targets from all gateways with gateway name attached', async () => { mockConfigExists.mockReturnValue(true); - mockReadMcpSpec.mockResolvedValue({ + mockReadProjectSpec.mockResolvedValue({ agentCoreGateways: [ { name: 'gateway-1', @@ -68,7 +74,7 @@ describe('getRemovableGatewayTargets', () => { it('returns empty array when no gateways', async () => { mockConfigExists.mockReturnValue(true); - mockReadMcpSpec.mockResolvedValue({ + mockReadProjectSpec.mockResolvedValue({ agentCoreGateways: [], }); @@ -79,7 +85,7 @@ describe('getRemovableGatewayTargets', () => { it('returns empty array when gateways have no targets', async () => { mockConfigExists.mockReturnValue(true); - mockReadMcpSpec.mockResolvedValue({ + mockReadProjectSpec.mockResolvedValue({ agentCoreGateways: [{ name: 'gateway-1', targets: [] }], }); @@ -93,7 +99,7 @@ describe('previewRemoveGatewayTarget', () => { afterEach(() => vi.clearAllMocks()); it('shows files that will be deleted for scaffolded targets', async () => { - mockReadMcpSpec.mockResolvedValue({ + mockReadProjectSpec.mockResolvedValue({ agentCoreGateways: [ { name: 'test-gateway', @@ -126,7 +132,7 @@ describe('previewRemoveGatewayTarget', () => { }); it('shows correct gateway name in preview', async () => { - mockReadMcpSpec.mockResolvedValue({ + mockReadProjectSpec.mockResolvedValue({ agentCoreGateways: [ { name: 'my-gateway', @@ -150,7 +156,7 @@ describe('previewRemoveGatewayTarget', () => { }); it('handles external targets with no files to delete', async () => { - mockReadMcpSpec.mockResolvedValue({ + mockReadProjectSpec.mockResolvedValue({ agentCoreGateways: [ { name: 'test-gateway', @@ -179,7 +185,7 @@ describe('previewRemoveGatewayTarget', () => { describe('removeGatewayTarget', () => { afterEach(() => vi.clearAllMocks()); - it('removes target from gateway config and writes updated mcp.json', async () => { + it('removes target from gateway config and writes updated agentcore.json', async () => { const mockMcpSpec = { agentCoreGateways: [ { @@ -188,7 +194,7 @@ describe('removeGatewayTarget', () => { }, ], }; - mockReadMcpSpec.mockResolvedValue(mockMcpSpec); + mockReadProjectSpec.mockResolvedValue(mockMcpSpec); mockConfigExists.mockReturnValue(true); mockReadMcpDefs.mockResolvedValue({ tools: {} }); mockGetProjectRoot.mockReturnValue('/project'); @@ -197,14 +203,16 @@ describe('removeGatewayTarget', () => { const result = await removeGatewayTarget(target); expect(result.success).toBe(true); - expect(mockWriteMcpSpec).toHaveBeenCalledWith({ - agentCoreGateways: [ - { - name: 'test-gateway', - targets: [{ name: 'target-2' }], - }, - ], - }); + expect(mockWriteProjectSpec).toHaveBeenCalledWith( + expect.objectContaining({ + agentCoreGateways: [ + { + name: 'test-gateway', + targets: [{ name: 'target-2' }], + }, + ], + }) + ); }); it('handles last target in gateway', async () => { @@ -216,7 +224,7 @@ describe('removeGatewayTarget', () => { }, ], }; - mockReadMcpSpec.mockResolvedValue(mockMcpSpec); + mockReadProjectSpec.mockResolvedValue(mockMcpSpec); mockConfigExists.mockReturnValue(true); mockReadMcpDefs.mockResolvedValue({ tools: {} }); mockGetProjectRoot.mockReturnValue('/project'); @@ -225,13 +233,15 @@ describe('removeGatewayTarget', () => { const result = await removeGatewayTarget(target); expect(result.success).toBe(true); - expect(mockWriteMcpSpec).toHaveBeenCalledWith({ - agentCoreGateways: [ - { - name: 'test-gateway', - targets: [], - }, - ], - }); + expect(mockWriteProjectSpec).toHaveBeenCalledWith( + expect.objectContaining({ + agentCoreGateways: [ + { + name: 'test-gateway', + targets: [], + }, + ], + }) + ); }); }); diff --git a/src/cli/operations/remove/__tests__/remove-identity-ops.test.ts b/src/cli/operations/remove/__tests__/remove-identity-ops.test.ts index 97ddf10b3..a1ba32e87 100644 --- a/src/cli/operations/remove/__tests__/remove-identity-ops.test.ts +++ b/src/cli/operations/remove/__tests__/remove-identity-ops.test.ts @@ -15,16 +15,19 @@ vi.mock('../../../../lib/index.js', () => ({ readProjectSpec = mockReadProjectSpec; writeProjectSpec = mockWriteProjectSpec; configExists = vi.fn().mockReturnValue(false); - readMcpSpec = vi.fn().mockResolvedValue({ agentCoreGateways: [] }); }, })); -const makeProject = (credNames: string[]) => ({ +const makeProject = ( + credNames: string[], + agentCoreGateways: { targets?: { outboundAuth?: { credentialName?: string } }[] }[] = [] +) => ({ name: 'TestProject', version: 1, agents: [], memories: [], credentials: credNames.map(name => ({ name, type: 'ApiKeyCredentialProvider' })), + agentCoreGateways, }); const primitive = new CredentialPrimitive(); diff --git a/src/cli/operations/remove/remove-gateway-target.ts b/src/cli/operations/remove/remove-gateway-target.ts index a3bf72c2f..0ceebf7eb 100644 --- a/src/cli/operations/remove/remove-gateway-target.ts +++ b/src/cli/operations/remove/remove-gateway-target.ts @@ -21,14 +21,11 @@ export interface RemovableGatewayTarget { export async function getRemovableGatewayTargets(): Promise { try { const configIO = new ConfigIO(); - if (!configIO.configExists('mcp')) { - return []; - } - const mcpSpec = await configIO.readMcpSpec(); + const project = await configIO.readProjectSpec(); const tools: RemovableGatewayTarget[] = []; // Gateway targets - for (const gateway of mcpSpec.agentCoreGateways) { + for (const gateway of project.agentCoreGateways) { for (const target of gateway.targets) { tools.push({ name: target.name, @@ -49,7 +46,12 @@ export async function getRemovableGatewayTargets(): Promise { const configIO = new ConfigIO(); - const mcpSpec = await configIO.readMcpSpec(); + const project = await configIO.readProjectSpec(); + const mcpSpec: AgentCoreMcpSpec = { + agentCoreGateways: project.agentCoreGateways, + mcpRuntimeTools: project.mcpRuntimeTools, + unassignedTargets: project.unassignedTargets, + }; const mcpDefs = configIO.configExists('mcpDefs') ? await configIO.readMcpDefs() : { tools: {} }; const summary: string[] = []; @@ -90,9 +92,9 @@ export async function previewRemoveGatewayTarget(tool: RemovableGatewayTarget): // Compute schema changes const afterMcpSpec = computeRemovedToolMcpSpec(mcpSpec, tool); schemaChanges.push({ - file: 'agentcore/mcp.json', - before: mcpSpec, - after: afterMcpSpec, + file: 'agentcore/agentcore.json', + before: project, + after: { ...project, ...afterMcpSpec }, }); const afterMcpDefs = computeRemovedToolMcpDefs(mcpSpec, mcpDefs, tool); @@ -156,7 +158,12 @@ function computeRemovedToolMcpDefs( export async function removeGatewayTarget(tool: RemovableGatewayTarget): Promise { try { const configIO = new ConfigIO(); - const mcpSpec = await configIO.readMcpSpec(); + const project = await configIO.readProjectSpec(); + const mcpSpec: AgentCoreMcpSpec = { + agentCoreGateways: project.agentCoreGateways, + mcpRuntimeTools: project.mcpRuntimeTools, + unassignedTargets: project.unassignedTargets, + }; const mcpDefs = configIO.configExists('mcpDefs') ? await configIO.readMcpDefs() : { tools: {} }; const projectRoot = configIO.getProjectRoot(); @@ -175,9 +182,9 @@ export async function removeGatewayTarget(tool: RemovableGatewayTarget): Promise toolPath = target.compute.implementation.path; } - // Update MCP spec + // Update project spec with MCP changes const newMcpSpec = computeRemovedToolMcpSpec(mcpSpec, tool); - await configIO.writeMcpSpec(newMcpSpec); + await configIO.writeProjectSpec({ ...project, ...newMcpSpec }); // Update MCP defs const newMcpDefs = computeRemovedToolMcpDefs(mcpSpec, mcpDefs, tool); diff --git a/src/cli/primitives/BasePrimitive.ts b/src/cli/primitives/BasePrimitive.ts index 2a5cd6e64..a0e13785c 100644 --- a/src/cli/primitives/BasePrimitive.ts +++ b/src/cli/primitives/BasePrimitive.ts @@ -13,9 +13,8 @@ import type { z } from 'zod'; * Each primitive (Agent, Memory, Credential, Gateway, GatewayTarget) * extends this class and owns its add/remove logic entirely. * - * The base provides shared helpers for the common case (agentcore.json), - * but primitives that use mcp.json override everything — they just share - * the same interface. + * The base provides shared helpers for reading/writing agentcore.json. + * All resource types (including gateways) now use agentcore.json. */ export abstract class BasePrimitive< TAddOptions = Record, diff --git a/src/cli/primitives/CredentialPrimitive.tsx b/src/cli/primitives/CredentialPrimitive.tsx index a2f912aee..7ef8ec083 100644 --- a/src/cli/primitives/CredentialPrimitive.tsx +++ b/src/cli/primitives/CredentialPrimitive.tsx @@ -1,5 +1,5 @@ import { findConfigRoot, getEnvVar, setEnvVar } from '../../lib'; -import type { AgentCoreMcpSpec, Credential, ModelProvider } from '../../schema'; +import type { Credential, ModelProvider } from '../../schema'; import { CredentialSchema } from '../../schema'; import { validateAddIdentityOptions } from '../commands/add/validate'; import { getErrorMessage } from '../errors'; @@ -421,22 +421,18 @@ export class CredentialPrimitive extends BasePrimitive { - if (!this.configIO.configExists('mcp')) { - return []; - } - - let mcpSpec: AgentCoreMcpSpec; + let project; try { - mcpSpec = await this.configIO.readMcpSpec(); + project = await this.readProjectSpec(); } catch { return []; } const referencingTargets: { name: string }[] = []; - for (const gateway of mcpSpec.agentCoreGateways) { + for (const gateway of project.agentCoreGateways) { for (const target of gateway.targets) { if (target.outboundAuth?.credentialName === credentialName) { referencingTargets.push({ name: target.name }); diff --git a/src/cli/primitives/GatewayPrimitive.ts b/src/cli/primitives/GatewayPrimitive.ts index 4f7cd2b79..54fea7f61 100644 --- a/src/cli/primitives/GatewayPrimitive.ts +++ b/src/cli/primitives/GatewayPrimitive.ts @@ -1,5 +1,11 @@ import { findConfigRoot, setEnvVar } from '../../lib'; -import type { AgentCoreGateway, AgentCoreGatewayTarget, AgentCoreMcpSpec, GatewayAuthorizerType } from '../../schema'; +import type { + AgentCoreGateway, + AgentCoreGatewayTarget, + AgentCoreMcpSpec, + AgentCoreProjectSpec, + GatewayAuthorizerType, +} from '../../schema'; import { AgentCoreGatewaySchema, PolicyEngineModeSchema } from '../../schema'; import type { AddGatewayOptions as CLIAddGatewayOptions } from '../commands/add/types'; import { validateAddGatewayOptions } from '../commands/add/validate'; @@ -32,10 +38,18 @@ export interface AddGatewayOptions { policyEngineMode?: string; } +/** Extract MCP-related fields from a project spec. */ +function extractMcpSpec(project: AgentCoreProjectSpec): AgentCoreMcpSpec { + return { + agentCoreGateways: project.agentCoreGateways, + mcpRuntimeTools: project.mcpRuntimeTools, + unassignedTargets: project.unassignedTargets, + }; +} + /** * GatewayPrimitive handles all gateway add/remove operations. * Absorbs logic from create-mcp.ts (gateway) and remove-gateway.ts. - * Uses mcp.json instead of agentcore.json. */ export class GatewayPrimitive extends BasePrimitive { readonly kind = 'gateway'; @@ -54,7 +68,8 @@ export class GatewayPrimitive extends BasePrimitive { try { - const mcpSpec = await this.configIO.readMcpSpec(); + const project = await this.readProjectSpec(); + const mcpSpec = extractMcpSpec(project); const gateway = mcpSpec.agentCoreGateways.find(g => g.name === gatewayName); if (!gateway) { @@ -62,7 +77,7 @@ export class GatewayPrimitive extends BasePrimitive { - const mcpSpec = await this.configIO.readMcpSpec(); + const project = await this.readProjectSpec(); + const mcpSpec = extractMcpSpec(project); const gateway = mcpSpec.agentCoreGateways.find(g => g.name === gatewayName); if (!gateway) { @@ -88,9 +104,9 @@ export class GatewayPrimitive extends BasePrimitive { try { - if (!this.configIO.configExists('mcp')) { - return []; - } - const mcpSpec = await this.configIO.readMcpSpec(); - return mcpSpec.agentCoreGateways.map(g => ({ name: g.name })); + const project = await this.readProjectSpec(); + return project.agentCoreGateways.map(g => ({ name: g.name })); } catch { return []; } @@ -113,26 +126,20 @@ export class GatewayPrimitive extends BasePrimitive { try { - if (!this.configIO.configExists('mcp')) { - return []; - } - const mcpSpec = await this.configIO.readMcpSpec(); - return mcpSpec.agentCoreGateways.map(g => g.name); + const project = await this.readProjectSpec(); + return project.agentCoreGateways.map(g => g.name); } catch { return []; } } /** - * Get list of unassigned targets from mcp.json. + * Get list of unassigned targets from agentcore.json. */ async getUnassignedTargets(): Promise { try { - if (!this.configIO.configExists('mcp')) { - return []; - } - const mcpSpec = await this.configIO.readMcpSpec(); - return mcpSpec.unassignedTargets ?? []; + const project = await this.readProjectSpec(); + return project.unassignedTargets ?? []; } catch { return []; } @@ -337,27 +344,25 @@ export class GatewayPrimitive extends BasePrimitive { - const mcpSpec: AgentCoreMcpSpec = this.configIO.configExists('mcp') - ? await this.configIO.readMcpSpec() - : { agentCoreGateways: [] }; + const project = await this.readProjectSpec(); - if (mcpSpec.agentCoreGateways.some(g => g.name === config.name)) { + if (project.agentCoreGateways.some(g => g.name === config.name)) { throw new Error(`Gateway "${config.name}" already exists.`); } // Move selected unassigned targets to the new gateway const selectedNames = new Set(config.selectedTargets ?? []); const movedTargets: AgentCoreGatewayTarget[] = []; - if (selectedNames.size > 0 && mcpSpec.unassignedTargets) { + if (selectedNames.size > 0 && project.unassignedTargets) { const remaining: AgentCoreGatewayTarget[] = []; - for (const target of mcpSpec.unassignedTargets) { + for (const target of project.unassignedTargets) { if (selectedNames.has(target.name)) { movedTargets.push(target); } else { remaining.push(target); } } - mcpSpec.unassignedTargets = remaining.length > 0 ? remaining : undefined; + project.unassignedTargets = remaining.length > 0 ? remaining : undefined; } const gateway: AgentCoreGateway = { @@ -371,8 +376,8 @@ export class GatewayPrimitive extends BasePrimitive { readonly kind = 'gateway-target'; @@ -84,14 +93,11 @@ export class GatewayTargetPrimitive extends BasePrimitive { try { - if (!this.configIO.configExists('mcp')) { - return []; - } - const mcpSpec = await this.configIO.readMcpSpec(); + const project = await this.readProjectSpec(); const tools: RemovableGatewayTarget[] = []; // Gateway targets - for (const gateway of mcpSpec.agentCoreGateways) { + for (const gateway of project.agentCoreGateways) { for (const target of gateway.targets) { tools.push({ name: target.name, @@ -111,7 +117,8 @@ export class GatewayTargetPrimitive extends BasePrimitive { - const mcpSpec = await this.configIO.readMcpSpec(); + const project = await this.readProjectSpec(); + const mcpSpec = extractMcpSpec(project); const mcpDefs = this.configIO.configExists('mcpDefs') ? await this.configIO.readMcpDefs() : { tools: {} }; const summary: string[] = []; @@ -148,9 +155,9 @@ export class GatewayTargetPrimitive extends BasePrimitive { try { - const mcpSpec = await this.configIO.readMcpSpec(); + const project = await this.readProjectSpec(); + const mcpSpec = extractMcpSpec(project); const mcpDefs = this.configIO.configExists('mcpDefs') ? await this.configIO.readMcpDefs() : { tools: {} }; const projectRoot = this.configIO.getProjectRoot(); @@ -189,9 +197,9 @@ export class GatewayTargetPrimitive extends BasePrimitive { try { - if (!this.configIO.configExists('mcp')) { - return []; - } - const mcpSpec = await this.configIO.readMcpSpec(); + const project = await this.readProjectSpec(); const toolNames: string[] = []; - for (const gateway of mcpSpec.agentCoreGateways) { + for (const gateway of project.agentCoreGateways) { for (const target of gateway.targets) { for (const toolDef of target.toolDefinitions ?? []) { toolNames.push(toolDef.name); @@ -527,9 +532,7 @@ export class GatewayTargetPrimitive extends BasePrimitive { - const mcpSpec: AgentCoreMcpSpec = this.configIO.configExists('mcp') - ? await this.configIO.readMcpSpec() - : { agentCoreGateways: [] }; + const project = await this.readProjectSpec(); const target: AgentCoreGatewayTarget = { name: config.name, @@ -545,7 +548,7 @@ export class GatewayTargetPrimitive extends BasePrimitive g.name === config.gateway); + const gateway = project.agentCoreGateways.find(g => g.name === config.gateway); if (!gateway) { throw new Error(`Gateway "${config.gateway}" not found.`); } @@ -557,7 +560,7 @@ export class GatewayTargetPrimitive extends BasePrimitive { - const mcpSpec: AgentCoreMcpSpec = this.configIO.configExists('mcp') - ? await this.configIO.readMcpSpec() - : { agentCoreGateways: [] }; + const project = await this.readProjectSpec(); - const gateway = mcpSpec.agentCoreGateways.find(g => g.name === config.gateway); + const gateway = project.agentCoreGateways.find(g => g.name === config.gateway); if (!gateway) { throw new Error(`Gateway "${config.gateway}" not found.`); } @@ -598,7 +599,7 @@ export class GatewayTargetPrimitive extends BasePrimitive { - const mcpSpec: AgentCoreMcpSpec = this.configIO.configExists('mcp') - ? await this.configIO.readMcpSpec() - : { agentCoreGateways: [] }; + const project = await this.readProjectSpec(); - const gateway = mcpSpec.agentCoreGateways.find(g => g.name === config.gateway); + const gateway = project.agentCoreGateways.find(g => g.name === config.gateway); if (!gateway) { throw new Error(`Gateway "${config.gateway}" not found.`); } @@ -629,7 +628,7 @@ export class GatewayTargetPrimitive extends BasePrimitive { - const mcpSpec: AgentCoreMcpSpec = this.configIO.configExists('mcp') - ? await this.configIO.readMcpSpec() - : { agentCoreGateways: [] }; + const project = await this.readProjectSpec(); - const gateway = mcpSpec.agentCoreGateways.find(g => g.name === config.gateway); + const gateway = project.agentCoreGateways.find(g => g.name === config.gateway); if (!gateway) { throw new Error(`Gateway "${config.gateway}" not found.`); } @@ -666,7 +663,7 @@ export class GatewayTargetPrimitive extends BasePrimitive { this.validateGatewayTargetLanguage(config.language!); - const mcpSpec: AgentCoreMcpSpec = this.configIO.configExists('mcp') - ? await this.configIO.readMcpSpec() - : { agentCoreGateways: [] }; + const project = await this.readProjectSpec(); const toolDefs = config.host === 'Lambda' ? getTemplateToolDefinitions(config.name, config.host) : [config.toolDefinition!]; @@ -713,7 +708,7 @@ export class GatewayTargetPrimitive extends BasePrimitive g.name === config.gateway); + const gateway = project.agentCoreGateways.find(g => g.name === config.gateway); if (!gateway) { throw new Error(`Gateway "${config.gateway}" not found.`); } @@ -770,7 +765,7 @@ export class GatewayTargetPrimitive extends BasePrimitive gw.policyEngineConfiguration?.policyEngineName === engineName + // Show changes if any gateways reference this engine + const affectedGateways = project.agentCoreGateways.filter( + gw => gw.policyEngineConfiguration?.policyEngineName === engineName + ); + if (affectedGateways.length > 0) { + summary.push( + `Note: ${affectedGateways.length} gateway(s) referencing this engine will have policyEngineConfiguration removed` + ); + summary.push( + 'Warning: this may grant agents escalated permissions to invoke gateway tools that were previously restricted' ); - if (affectedGateways.length > 0) { - summary.push( - `Note: ${affectedGateways.length} gateway(s) referencing this engine will have policyEngineConfiguration removed` - ); - summary.push( - 'Warning: this may grant agents escalated permissions to invoke gateway tools that were previously restricted' - ); - const afterMcpSpec = { - ...mcpSpec, - agentCoreGateways: mcpSpec.agentCoreGateways.map(gw => - gw.policyEngineConfiguration?.policyEngineName === engineName - ? { ...gw, policyEngineConfiguration: undefined } - : gw - ), - }; - schemaChanges.push({ - file: 'agentcore/mcp.json', - before: mcpSpec, - after: afterMcpSpec, - }); - } } return { summary, directoriesToDelete: [], schemaChanges }; @@ -154,9 +135,8 @@ export class PolicyEnginePrimitive extends BasePrimitive { try { - if (!this.configIO.configExists('mcp')) return []; - const mcpSpec = await this.configIO.readMcpSpec(); - return mcpSpec.agentCoreGateways.filter(gw => !gw.policyEngineConfiguration).map(gw => gw.name); + const project = await this.readProjectSpec(); + return project.agentCoreGateways.filter(gw => !gw.policyEngineConfiguration).map(gw => gw.name); } catch { return []; } @@ -166,15 +146,15 @@ export class PolicyEnginePrimitive extends BasePrimitive { - if (gatewayNames.length === 0 || !this.configIO.configExists('mcp')) return; - const mcpSpec = await this.configIO.readMcpSpec(); + if (gatewayNames.length === 0) return; + const project = await this.readProjectSpec(); const nameSet = new Set(gatewayNames); - for (const gw of mcpSpec.agentCoreGateways) { + for (const gw of project.agentCoreGateways) { if (nameSet.has(gw.name)) { gw.policyEngineConfiguration = { policyEngineName: engineName, mode }; } } - await this.configIO.writeMcpSpec(mcpSpec); + await this.writeProjectSpec(project); } async getDeployedEngineId(engineName: string): Promise { diff --git a/src/cli/primitives/__tests__/GatewayPrimitive.test.ts b/src/cli/primitives/__tests__/GatewayPrimitive.test.ts index 14a5ccd7d..cbe8e6c65 100644 --- a/src/cli/primitives/__tests__/GatewayPrimitive.test.ts +++ b/src/cli/primitives/__tests__/GatewayPrimitive.test.ts @@ -1,18 +1,30 @@ -import type { AgentCoreMcpSpec } from '../../../schema'; +import type { AgentCoreProjectSpec } from '../../../schema'; import { GatewayPrimitive } from '../GatewayPrimitive'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -const { mockConfigExists, mockReadMcpSpec, mockWriteMcpSpec } = vi.hoisted(() => ({ +const defaultProject: AgentCoreProjectSpec = { + name: 'test', + version: 1, + agents: [], + memories: [], + credentials: [], + evaluators: [], + onlineEvalConfigs: [], + agentCoreGateways: [], + policyEngines: [], +}; + +const { mockConfigExists, mockReadProjectSpec, mockWriteProjectSpec } = vi.hoisted(() => ({ mockConfigExists: vi.fn().mockReturnValue(true), - mockReadMcpSpec: vi.fn().mockResolvedValue({ agentCoreGateways: [] }), - mockWriteMcpSpec: vi.fn().mockResolvedValue(undefined), + mockReadProjectSpec: vi.fn(), + mockWriteProjectSpec: vi.fn().mockResolvedValue(undefined), })); vi.mock('../../../lib', () => { const MockConfigIO = vi.fn(function (this: Record) { this.configExists = mockConfigExists; - this.readMcpSpec = mockReadMcpSpec; - this.writeMcpSpec = mockWriteMcpSpec; + this.readProjectSpec = mockReadProjectSpec; + this.writeProjectSpec = mockWriteProjectSpec; }); return { ConfigIO: MockConfigIO, @@ -21,10 +33,10 @@ vi.mock('../../../lib', () => { }; }); -/** Extract the first gateway written to writeMcpSpec. */ +/** Extract the first gateway written to writeProjectSpec. */ function getWrittenGateway() { - expect(mockWriteMcpSpec).toHaveBeenCalledTimes(1); - const spec = mockWriteMcpSpec.mock.calls[0]![0] as AgentCoreMcpSpec; + expect(mockWriteProjectSpec).toHaveBeenCalledTimes(1); + const spec = mockWriteProjectSpec.mock.calls[0]![0] as AgentCoreProjectSpec; const gw = spec.agentCoreGateways[0]; expect(gw).toBeDefined(); return gw!; @@ -35,7 +47,7 @@ describe('GatewayPrimitive', () => { beforeEach(() => { vi.clearAllMocks(); - mockReadMcpSpec.mockResolvedValue({ agentCoreGateways: [] }); + mockReadProjectSpec.mockImplementation(() => Promise.resolve({ ...defaultProject, agentCoreGateways: [] })); primitive = new GatewayPrimitive(); }); diff --git a/src/cli/tui/screens/create/useCreateFlow.ts b/src/cli/tui/screens/create/useCreateFlow.ts index cb83f604b..5254e921c 100644 --- a/src/cli/tui/screens/create/useCreateFlow.ts +++ b/src/cli/tui/screens/create/useCreateFlow.ts @@ -76,6 +76,7 @@ function createDefaultProjectSpec(projectName: string): AgentCoreProjectSpec { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; } diff --git a/src/cli/tui/screens/deploy/DeployScreen.tsx b/src/cli/tui/screens/deploy/DeployScreen.tsx index 1117b374d..f08d24793 100644 --- a/src/cli/tui/screens/deploy/DeployScreen.tsx +++ b/src/cli/tui/screens/deploy/DeployScreen.tsx @@ -1,5 +1,5 @@ import { ConfigIO } from '../../../../lib'; -import type { AgentCoreMcpSpec } from '../../../../schema'; +import type { AgentCoreMcpSpec, AgentCoreProjectSpec } from '../../../../schema'; import { formatTargetStatus } from '../../../operations/deploy/gateway-status'; import { AwsTargetConfigUI, @@ -99,15 +99,21 @@ export function DeployScreen({ const allSuccess = !hasError && isComplete; const skipPreflight = !!preSynthesized; - // Load MCP spec when context is available + // Extract MCP spec from project when context is available useEffect(() => { if (!context) return; - if (configIO.configExists('mcp')) { - configIO - .readMcpSpec() - .then(setMcpSpec) - .catch(() => setMcpSpec(undefined)); - } + configIO + .readProjectSpec() + .then((project: AgentCoreProjectSpec) => { + if (project.agentCoreGateways?.length) { + setMcpSpec({ + agentCoreGateways: project.agentCoreGateways, + mcpRuntimeTools: project.mcpRuntimeTools, + unassignedTargets: project.unassignedTargets, + }); + } + }) + .catch(() => setMcpSpec(undefined)); }, [context, configIO]); // Toggle ResourceGraph with Ctrl+G diff --git a/src/cli/tui/screens/deploy/useDeployFlow.ts b/src/cli/tui/screens/deploy/useDeployFlow.ts index 7288fe1f8..2a6e3b9bb 100644 --- a/src/cli/tui/screens/deploy/useDeployFlow.ts +++ b/src/cli/tui/screens/deploy/useDeployFlow.ts @@ -239,9 +239,9 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState // Parse gateway outputs from CDK stack let gateways: Record = {}; try { - const mcpSpec = await configIO.readMcpSpec(); + const projectForGateways = await configIO.readProjectSpec(); const gatewaySpecs = - mcpSpec?.agentCoreGateways?.reduce( + projectForGateways.agentCoreGateways?.reduce( (acc: Record, gateway: { name: string }) => { acc[gateway.name] = gateway; return acc; @@ -407,14 +407,7 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState const agentNames = context?.projectSpec.agents?.map((a: { name: string }) => a.name) ?? []; const targetRegion = context?.awsTargets[0]?.region; const targetAccount = context?.awsTargets[0]?.account; - let hasGateways = false; - try { - const tsConfigIO = new ConfigIO(); - const mcpSpec = await tsConfigIO.readMcpSpec(); - hasGateways = (mcpSpec?.agentCoreGateways?.length ?? 0) > 0; - } catch { - // No mcp.json or invalid -- no gateways - } + const hasGateways = (context?.projectSpec.agentCoreGateways?.length ?? 0) > 0; if ((agentNames.length > 0 || hasGateways) && targetRegion && targetAccount) { try { const tsResult = await setupTransactionSearch({ diff --git a/src/cli/tui/screens/mcp/AddGatewayFlow.tsx b/src/cli/tui/screens/mcp/AddGatewayFlow.tsx index 5bf5ded42..8f00be43d 100644 --- a/src/cli/tui/screens/mcp/AddGatewayFlow.tsx +++ b/src/cli/tui/screens/mcp/AddGatewayFlow.tsx @@ -80,7 +80,7 @@ export function AddGatewayFlow({ isInteractive = true, onExit, onBack, onDev, on { resetAll(); diff --git a/src/cli/tui/screens/remove/useRemoveFlow.ts b/src/cli/tui/screens/remove/useRemoveFlow.ts index dd1672149..0051f9f81 100644 --- a/src/cli/tui/screens/remove/useRemoveFlow.ts +++ b/src/cli/tui/screens/remove/useRemoveFlow.ts @@ -36,6 +36,7 @@ function createDefaultProjectSpec(projectName: string): AgentCoreProjectSpec { credentials: [], evaluators: [], onlineEvalConfigs: [], + agentCoreGateways: [], policyEngines: [], }; } @@ -79,6 +80,18 @@ export function useRemoveFlow({ force, dryRun }: RemoveFlowOptions): RemoveFlowS if (projectSpec.credentials && projectSpec.credentials.length > 0) { items.push(`${projectSpec.credentials.length} credential${projectSpec.credentials.length > 1 ? 's' : ''}`); } + // Check for gateways in agentcore.json + const gatewayCount = projectSpec.agentCoreGateways?.length ?? 0; + if (gatewayCount > 0) { + const targetCount = projectSpec.agentCoreGateways.reduce( + (sum: number, gw: { targets?: unknown[] }) => sum + (gw.targets?.length ?? 0), + 0 + ); + items.push(`${gatewayCount} gateway${gatewayCount > 1 ? 's' : ''}`); + if (targetCount > 0) { + items.push(`${targetCount} gateway target${targetCount > 1 ? 's' : ''}`); + } + } if (projectSpec.policyEngines && projectSpec.policyEngines.length > 0) { items.push( `${projectSpec.policyEngines.length} policy engine${projectSpec.policyEngines.length > 1 ? 's' : ''}` @@ -93,26 +106,6 @@ export function useRemoveFlow({ force, dryRun }: RemoveFlowOptions): RemoveFlowS items.push('AgentCore project (corrupted or incomplete)'); } - // Check for gateways in mcp.json - if (configIO.configExists('mcp')) { - try { - const mcpSpec = await configIO.readMcpSpec(); - const gatewayCount = mcpSpec.agentCoreGateways?.length ?? 0; - if (gatewayCount > 0) { - const targetCount = mcpSpec.agentCoreGateways.reduce( - (sum: number, gw: { targets?: unknown[] }) => sum + (gw.targets?.length ?? 0), - 0 - ); - items.push(`${gatewayCount} gateway${gatewayCount > 1 ? 's' : ''}`); - if (targetCount > 0) { - items.push(`${targetCount} gateway target${targetCount > 1 ? 's' : ''}`); - } - } - } catch { - // mcp.json exists but has issues - still allow reset - } - } - items.push('All schemas will be reset to empty state'); setItemsToRemove(items); @@ -174,11 +167,6 @@ export function useRemoveFlow({ force, dryRun }: RemoveFlowOptions): RemoveFlowS const defaultProjectSpec = createDefaultProjectSpec(projectName || 'Project'); await configIO.writeProjectSpec(defaultProjectSpec); - // Reset mcp.json gateways so a subsequent deploy can tear down gateway resources - if (configIO.configExists('mcp')) { - await configIO.writeMcpSpec({ agentCoreGateways: [] }); - } - // Preserve aws-targets.json and deployed-state.json so that // a subsequent `agentcore deploy` can tear down existing stacks. }); diff --git a/src/cli/tui/screens/schema/EditSchemaScreen.tsx b/src/cli/tui/screens/schema/EditSchemaScreen.tsx index 1721f5f80..4df5dea6c 100644 --- a/src/cli/tui/screens/schema/EditSchemaScreen.tsx +++ b/src/cli/tui/screens/schema/EditSchemaScreen.tsx @@ -1,5 +1,5 @@ import { ConfigIO } from '../../../../lib'; -import { AgentCoreMcpSpecSchema, type AgentCoreProjectSpec, AgentCoreProjectSpecSchema } from '../../../../schema'; +import { type AgentCoreProjectSpec, AgentCoreProjectSpecSchema } from '../../../../schema'; import { loadSchemaDocument } from '../../../schema'; import { ErrorPrompt, SelectScreen } from '../../components'; import { AgentCoreGuidedEditor } from './AgentCoreGuidedEditor'; @@ -27,11 +27,9 @@ export function EditSchemaScreen(props: EditSchemaScreenProps) { const _isInteractive = props.isInteractive; const configIO = useMemo(() => new ConfigIO(), []); const pathResolver = configIO.getPathResolver(); - const mcpPath = pathResolver.getMcpConfigPath(); const schemaOptions = useMemo(() => { const projectMissing = configIO.configExists('project') ? '' : ' - missing'; - const mcpMissing = configIO.configExists('mcp') ? '' : ' - missing'; return [ { @@ -43,13 +41,13 @@ export function EditSchemaScreen(props: EditSchemaScreenProps) { }, { id: 'mcp', - title: 'mcp.json', - description: `Gateways and tools${mcpMissing}`, - filePath: mcpPath, - schema: AgentCoreMcpSpecSchema, + title: 'Gateways & tools', + description: 'Gateway and MCP tool configuration (in agentcore.json)', + filePath: pathResolver.getAgentConfigPath(), + schema: AgentCoreProjectSpecSchema, }, ]; - }, [pathResolver, configIO, mcpPath]); + }, [pathResolver, configIO]); const [activeSchema, setActiveSchema] = useState(null); const [errorPrompt, setErrorPrompt] = useState<{ message: string; detail?: string } | null>(null); diff --git a/src/cli/tui/screens/schema/McpGuidedEditor.tsx b/src/cli/tui/screens/schema/McpGuidedEditor.tsx index 8cb2c3361..a5609f3b5 100644 --- a/src/cli/tui/screens/schema/McpGuidedEditor.tsx +++ b/src/cli/tui/screens/schema/McpGuidedEditor.tsx @@ -2,10 +2,12 @@ import { type AgentCoreGateway, type AgentCoreGatewayTarget, type AgentCoreMcpSpec, - AgentCoreMcpSpecSchema, + type AgentCoreProjectSpec, + AgentCoreProjectSpecSchema, GatewayNameSchema, type OutboundAuth, } from '../../../../schema'; +import type { SaveDocumentResult } from '../../../schema'; import { Header, Panel, ScreenLayout, TextInput } from '../../components'; import { useSchemaDocument } from '../../hooks/useSchemaDocument'; import { diffLines } from '../../utils'; @@ -25,7 +27,7 @@ interface McpGuidedEditorProps { } export function McpGuidedEditor(props: McpGuidedEditorProps) { - const { content, status, save } = useSchemaDocument(props.schema.filePath, AgentCoreMcpSpecSchema); + const { content, status, save: rawSave } = useSchemaDocument(props.schema.filePath, AgentCoreProjectSpecSchema); if (status.status === 'loading') { return ( @@ -41,7 +43,7 @@ export function McpGuidedEditor(props: McpGuidedEditorProps) {
- Unable to load mcp.json + Unable to load agentcore.json {status.message ?? 'Unknown error'} Esc back @@ -49,20 +51,34 @@ export function McpGuidedEditor(props: McpGuidedEditorProps) { ); } + let projectSpec: AgentCoreProjectSpec | null = null; let mcpSpec: AgentCoreMcpSpec & { unassignedTargets?: AgentCoreGatewayTarget[] } = { agentCoreGateways: [], unassignedTargets: [], }; try { const parsed: unknown = JSON.parse(content); - const result = AgentCoreMcpSpecSchema.safeParse(parsed); + const result = AgentCoreProjectSpecSchema.safeParse(parsed); if (result.success) { - mcpSpec = result.data; + projectSpec = result.data; + mcpSpec = { + agentCoreGateways: result.data.agentCoreGateways, + mcpRuntimeTools: result.data.mcpRuntimeTools, + unassignedTargets: result.data.unassignedTargets, + }; } } catch { // Will show empty gateways } + // Wrap save to merge MCP fields back into the full project spec + const save = async (mcpContent: string): Promise => { + if (!projectSpec) return { ok: false, error: 'No project spec loaded' }; + const mcpData = JSON.parse(mcpContent) as AgentCoreMcpSpec; + const merged = { ...projectSpec, ...mcpData }; + return rawSave(JSON.stringify(merged, null, 2)); + }; + const baseline = JSON.stringify(mcpSpec, null, 2); return ( diff --git a/src/cli/tui/screens/status/StatusScreen.tsx b/src/cli/tui/screens/status/StatusScreen.tsx index db0f78c14..313f58710 100644 --- a/src/cli/tui/screens/status/StatusScreen.tsx +++ b/src/cli/tui/screens/status/StatusScreen.tsx @@ -18,7 +18,6 @@ export function StatusScreen({ isInteractive: _isInteractive, onExit }: StatusSc targetName, targetRegion, hasMultipleTargets, - mcpSpec, resourceStatuses, statusesLoading, statusesError, @@ -92,7 +91,21 @@ export function StatusScreen({ isInteractive: _isInteractive, onExit }: StatusSc )} - {project && } + {project && ( + + )} ); } diff --git a/src/cli/tui/screens/status/useStatusFlow.ts b/src/cli/tui/screens/status/useStatusFlow.ts index 7b315d293..2260e1e82 100644 --- a/src/cli/tui/screens/status/useStatusFlow.ts +++ b/src/cli/tui/screens/status/useStatusFlow.ts @@ -1,4 +1,4 @@ -import type { AgentCoreMcpSpec, AgentCoreProjectSpec, AwsDeploymentTargets, DeployedState } from '../../../../schema'; +import type { AgentCoreProjectSpec, AwsDeploymentTargets, DeployedState } from '../../../../schema'; import type { ResourceStatusEntry, StatusContext } from '../../../commands/status/action'; import { handleProjectStatus, loadStatusConfig } from '../../../commands/status/action'; import { getErrorMessage } from '../../../errors'; @@ -12,7 +12,6 @@ interface StatusState { project?: AgentCoreProjectSpec; deployedState?: DeployedState; awsTargets?: AwsDeploymentTargets; - mcpSpec?: AgentCoreMcpSpec; targetIndex: number; resourceStatuses: ResourceStatusEntry[]; statusesLoaded: boolean; @@ -43,7 +42,6 @@ export function useStatusFlow() { project: context.project, deployedState: context.deployedState, awsTargets: context.awsTargets, - mcpSpec: context.mcpSpec, })); }) .catch((error: Error) => { @@ -62,9 +60,8 @@ export function useStatusFlow() { project: state.project, deployedState: state.deployedState, awsTargets: state.awsTargets, - mcpSpec: state.mcpSpec, }; - }, [state.awsTargets, state.deployedState, state.mcpSpec, state.project]); + }, [state.awsTargets, state.deployedState, state.project]); // Derive target names — fall back to awsTargets when deployedState is empty const targetNames = useMemo(() => { @@ -157,7 +154,6 @@ export function useStatusFlow() { targetName: targetName ?? 'No target configured', targetRegion: targetConfig?.region, hasMultipleTargets: targetNames.length > 1, - mcpSpec: state.mcpSpec, resourceStatuses: state.resourceStatuses, statusesLoading: state.phase === 'fetching-statuses', statusesError: state.statusesError, diff --git a/src/cli/tui/screens/validate/ValidateScreen.tsx b/src/cli/tui/screens/validate/ValidateScreen.tsx index 24c0c743a..bc9e98c41 100644 --- a/src/cli/tui/screens/validate/ValidateScreen.tsx +++ b/src/cli/tui/screens/validate/ValidateScreen.tsx @@ -22,7 +22,6 @@ interface ValidationState { const SCHEMA_FILES = [ { key: 'project', label: 'agentcore.json', required: true }, { key: 'targets', label: 'aws-targets.json', required: true }, - { key: 'mcp', label: 'mcp.json', required: false }, { key: 'mcpDefs', label: 'mcp-defs.json', required: false }, { key: 'state', label: '.cli/state.json', required: false }, ] as const; @@ -77,13 +76,6 @@ export function ValidateScreen({ isInteractive, onExit }: ValidateScreenProps) { } else if (file.key === 'targets') { await configIO.readAWSDeploymentTargets(); newSteps[i] = { label: file.label, status: 'success' }; - } else if (file.key === 'mcp') { - if (configIO.configExists('mcp')) { - await configIO.readMcpSpec(); - newSteps[i] = { label: file.label, status: 'success' }; - } else { - newSteps[i] = { label: file.label, status: 'info', info: 'Not present (optional)' }; - } } else if (file.key === 'mcpDefs') { if (configIO.configExists('mcpDefs')) { await configIO.readMcpDefs(); diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 8e937b37b..fac7f0a9c 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -18,7 +18,6 @@ export const CONFIG_FILES = { AGENT_ENV: 'agentcore.json', AWS_TARGETS: 'aws-targets.json', DEPLOYED_STATE: 'deployed-state.json', - MCP: 'mcp.json', MCP_DEFS: 'mcp-defs.json', } as const; diff --git a/src/lib/schemas/io/__tests__/config-io.test.ts b/src/lib/schemas/io/__tests__/config-io.test.ts index a3fdc0547..1da80af77 100644 --- a/src/lib/schemas/io/__tests__/config-io.test.ts +++ b/src/lib/schemas/io/__tests__/config-io.test.ts @@ -87,18 +87,11 @@ describe('ConfigIO', () => { expect(existsSync(join(emptyDir, 'agentcore'))).toBe(false); }); - it('writeMcpSpec() throws NoProjectError when no project exists', async () => { - const configIO = new ConfigIO(); - await expect(configIO.writeMcpSpec({ agentCoreGateways: [] })).rejects.toThrow(NoProjectError); - expect(existsSync(join(emptyDir, 'agentcore'))).toBe(false); - }); - it('does not create agentcore directory on any write operation', async () => { const configIO = new ConfigIO(); const operations = [ () => configIO.initializeBaseDir(), () => configIO.writeProjectSpec({ version: '1.0', agents: [] } as never), - () => configIO.writeMcpSpec({ agentCoreGateways: [] }), () => configIO.writeMcpDefs({ tools: {} }), ]; @@ -231,7 +224,6 @@ describe('ConfigIO', () => { const configIO = new ConfigIO(); expect(configIO.configExists('awsTargets')).toBe(false); expect(configIO.configExists('state')).toBe(false); - expect(configIO.configExists('mcp')).toBe(false); expect(configIO.configExists('mcpDefs')).toBe(false); }); }); @@ -275,23 +267,6 @@ describe('ConfigIO', () => { }); }); - describe('writeMcpSpec and readMcpSpec', () => { - it('round-trips a valid MCP spec', async () => { - const projectDir = join(testDir, `mcp-rt-${randomUUID()}`); - const agentcoreDir = join(projectDir, 'agentcore'); - mkdirSync(agentcoreDir, { recursive: true }); - - const configIO = new ConfigIO({ baseDir: agentcoreDir }); - - const mcpSpec = { agentCoreGateways: [] }; - await configIO.writeMcpSpec(mcpSpec); - expect(configIO.configExists('mcp')).toBe(true); - - const readBack = await configIO.readMcpSpec(); - expect(readBack.agentCoreGateways).toEqual([]); - }); - }); - describe('writeMcpDefs and readMcpDefs', () => { it('round-trips valid MCP definitions', async () => { const projectDir = join(testDir, `mcpdefs-rt-${randomUUID()}`); diff --git a/src/lib/schemas/io/__tests__/path-resolver.test.ts b/src/lib/schemas/io/__tests__/path-resolver.test.ts index 6d00c35ac..cbf9c8288 100644 --- a/src/lib/schemas/io/__tests__/path-resolver.test.ts +++ b/src/lib/schemas/io/__tests__/path-resolver.test.ts @@ -199,11 +199,6 @@ describe('PathResolver', () => { expect(resolver.getStatePath()).toBe(join('/base', '.cli', 'deployed-state.json')); }); - it('getMcpConfigPath returns mcp.json path', () => { - const resolver = new PathResolver({ baseDir: '/base' }); - expect(resolver.getMcpConfigPath()).toBe(join('/base', 'mcp.json')); - }); - it('getMcpDefsPath returns mcp-defs.json path', () => { const resolver = new PathResolver({ baseDir: '/base' }); expect(resolver.getMcpDefsPath()).toBe(join('/base', 'mcp-defs.json')); diff --git a/src/lib/schemas/io/config-io.ts b/src/lib/schemas/io/config-io.ts index afc2f2f9a..5d6c4f665 100644 --- a/src/lib/schemas/io/config-io.ts +++ b/src/lib/schemas/io/config-io.ts @@ -1,13 +1,6 @@ -import type { - AgentCoreCliMcpDefs, - AgentCoreMcpSpec, - AgentCoreProjectSpec, - AwsDeploymentTarget, - DeployedState, -} from '../../../schema'; +import type { AgentCoreCliMcpDefs, AgentCoreProjectSpec, AwsDeploymentTarget, DeployedState } from '../../../schema'; import { AgentCoreCliMcpDefsSchema, - AgentCoreMcpSpecSchema, AgentCoreProjectSpecSchema, AgentCoreRegionSchema, AwsDeploymentTargetsSchema, @@ -186,22 +179,6 @@ export class ConfigIO { await this.validateAndWrite(filePath, 'State', schema, data); } - /** - * Read and validate the MCP configuration file - */ - async readMcpSpec(): Promise { - const filePath = this.pathResolver.getMcpConfigPath(); - return this.readAndValidate(filePath, 'MCP Config', AgentCoreMcpSpecSchema); - } - - /** - * Write and validate the MCP configuration file - */ - async writeMcpSpec(data: AgentCoreMcpSpec): Promise { - const filePath = this.pathResolver.getMcpConfigPath(); - await this.validateAndWrite(filePath, 'MCP Config', AgentCoreMcpSpecSchema, data); - } - /** * Read and validate the MCP definitions file */ @@ -228,12 +205,11 @@ export class ConfigIO { /** * Check if a specific config file exists */ - configExists(type: 'project' | 'awsTargets' | 'state' | 'mcp' | 'mcpDefs'): boolean { + configExists(type: 'project' | 'awsTargets' | 'state' | 'mcpDefs'): boolean { const pathMap = { project: this.pathResolver.getAgentConfigPath(), awsTargets: this.pathResolver.getAWSTargetsConfigPath(), state: this.pathResolver.getStatePath(), - mcp: this.pathResolver.getMcpConfigPath(), mcpDefs: this.pathResolver.getMcpDefsPath(), }; return existsSync(pathMap[type]); diff --git a/src/lib/schemas/io/path-resolver.ts b/src/lib/schemas/io/path-resolver.ts index 45d2fd0cc..9737c4a2b 100644 --- a/src/lib/schemas/io/path-resolver.ts +++ b/src/lib/schemas/io/path-resolver.ts @@ -195,13 +195,6 @@ export class PathResolver { return join(this.config.baseDir, CLI_SYSTEM_DIR, CONFIG_FILES.DEPLOYED_STATE); } - /** - * Get the path to the MCP config file (mcp.json) - */ - getMcpConfigPath(): string { - return join(this.config.baseDir, CONFIG_FILES.MCP); - } - /** * Get the path to the MCP definitions file (mcp-defs.json) */ diff --git a/src/schema/llm-compacted/mcp.ts b/src/schema/llm-compacted/mcp.ts index 58fbd9102..dd6fafc27 100644 --- a/src/schema/llm-compacted/mcp.ts +++ b/src/schema/llm-compacted/mcp.ts @@ -2,7 +2,7 @@ /** * READ-ONLY LLM CONTEXT - Do not edit this file. * - * JSON File: agentcore/mcp.json + * These MCP/Gateway types are now part of agentcore/agentcore.json (merged from former mcp.json). * Purpose: MCP gateways, tool definitions, and compute configurations */ diff --git a/src/schema/schemas/__tests__/mcp.test.ts b/src/schema/schemas/__tests__/mcp.test.ts index 517683a21..4295a8b2e 100644 --- a/src/schema/schemas/__tests__/mcp.test.ts +++ b/src/schema/schemas/__tests__/mcp.test.ts @@ -2,7 +2,6 @@ import { AgentCoreGatewaySchema, AgentCoreGatewayTargetSchema, AgentCoreMcpRuntimeToolSchema, - AgentCoreMcpSpecSchema, ApiGatewayConfigSchema, CustomJwtAuthorizerConfigSchema, GatewayAuthorizerTypeSchema, @@ -991,74 +990,3 @@ describe('AgentCoreGatewayTargetSchema with outbound auth', () => { expect(result.success).toBe(false); }); }); - -describe('AgentCoreMcpSpecSchema', () => { - it('accepts valid MCP spec', () => { - const validToolDef = { - name: 'tool', - description: 'A tool', - inputSchema: { type: 'object' as const }, - }; - - const result = AgentCoreMcpSpecSchema.safeParse({ - agentCoreGateways: [ - { - name: 'gw1', - targets: [ - { - name: 't1', - targetType: 'lambda', - toolDefinitions: [validToolDef], - compute: { - host: 'Lambda', - implementation: { language: 'Python', path: 'tools', handler: 'h' }, - pythonVersion: 'PYTHON_3_12', - }, - }, - ], - }, - ], - }); - expect(result.success).toBe(true); - }); - - it('rejects extra fields (strict)', () => { - const result = AgentCoreMcpSpecSchema.safeParse({ - agentCoreGateways: [], - unknownField: true, - }); - expect(result.success).toBe(false); - }); - - it('spec with unassignedTargets array parses correctly', () => { - const validToolDef = { - name: 'tool', - description: 'A tool', - inputSchema: { type: 'object' as const }, - }; - - const result = AgentCoreMcpSpecSchema.safeParse({ - agentCoreGateways: [], - unassignedTargets: [ - { - name: 'unassigned-target', - targetType: 'lambda', - toolDefinitions: [validToolDef], - compute: { - host: 'Lambda', - implementation: { language: 'Python', path: 'tools', handler: 'h' }, - pythonVersion: 'PYTHON_3_12', - }, - }, - ], - }); - expect(result.success).toBe(true); - }); - - it('spec without unassignedTargets parses correctly', () => { - const result = AgentCoreMcpSpecSchema.safeParse({ - agentCoreGateways: [], - }); - expect(result.success).toBe(true); - }); -}); diff --git a/src/schema/schemas/agentcore-project.ts b/src/schema/schemas/agentcore-project.ts index 7aafe024e..50ffbf495 100644 --- a/src/schema/schemas/agentcore-project.ts +++ b/src/schema/schemas/agentcore-project.ts @@ -8,6 +8,7 @@ */ import { isReservedProjectName } from '../constants'; import { AgentEnvSpecSchema } from './agent-env'; +import { AgentCoreGatewaySchema, AgentCoreGatewayTargetSchema, AgentCoreMcpRuntimeToolSchema } from './mcp'; import { EvaluationLevelSchema, EvaluatorConfigSchema, EvaluatorNameSchema } from './primitives/evaluator'; import { DEFAULT_STRATEGY_NAMESPACES, MemoryStrategySchema, MemoryStrategyTypeSchema } from './primitives/memory'; import { OnlineEvalConfigSchema } from './primitives/online-eval-config'; @@ -27,6 +28,10 @@ export { PolicyEngineSchema }; export type { Policy, PolicyEngine, ValidationMode } from './primitives/policy'; export { PolicyEngineNameSchema, PolicyNameSchema, PolicySchema, ValidationModeSchema } from './primitives/policy'; +// Re-export MCP types (now part of unified schema) +export type { AgentCoreGateway, AgentCoreGatewayTarget, AgentCoreMcpRuntimeTool } from './mcp'; +export { AgentCoreGatewaySchema, AgentCoreGatewayTargetSchema, AgentCoreMcpRuntimeToolSchema } from './mcp'; + // ============================================================================ // Project Name Schema // ============================================================================ @@ -201,6 +206,39 @@ export const AgentCoreProjectSpecSchema = z ) ), + // MCP / Gateway resources (previously in mcp.json) + agentCoreGateways: z + .array(AgentCoreGatewaySchema) + .default([]) + .superRefine( + uniqueBy( + gateway => gateway.name, + name => `Duplicate gateway name: ${name}` + ) + ), + + mcpRuntimeTools: z + .array(AgentCoreMcpRuntimeToolSchema) + .optional() + .superRefine((tools, ctx) => { + if (!tools) return; + uniqueBy( + (tool: { name: string }) => tool.name, + (name: string) => `Duplicate MCP runtime tool name: ${name}` + )(tools, ctx); + }), + + unassignedTargets: z + .array(AgentCoreGatewayTargetSchema) + .optional() + .superRefine((targets, ctx) => { + if (!targets) return; + uniqueBy( + (target: { name: string }) => target.name, + (name: string) => `Duplicate unassigned target name: ${name}` + )(targets, ctx); + }), + policyEngines: z .array(PolicyEngineSchema) .default([]) @@ -211,6 +249,7 @@ export const AgentCoreProjectSpecSchema = z ) ), }) + .strict() .superRefine((spec, ctx) => { const agentNames = new Set(spec.agents.map(a => a.name)); const evaluatorNames = new Set(spec.evaluators.map(e => e.name)); diff --git a/src/schema/schemas/mcp.ts b/src/schema/schemas/mcp.ts index 6d826f9fa..7e951c0f2 100644 --- a/src/schema/schemas/mcp.ts +++ b/src/schema/schemas/mcp.ts @@ -689,18 +689,15 @@ export const AgentCoreMcpRuntimeToolSchema = z export type AgentCoreMcpRuntimeTool = z.infer; // ============================================================================ -// Top-Level MCP Spec +// MCP Spec Type (convenience alias) // ============================================================================ /** - * Top-level MCP schema. + * Shape of MCP-related fields within AgentCoreProjectSpec. + * These fields are now part of agentcore.json (previously in mcp.json). */ -export const AgentCoreMcpSpecSchema = z - .object({ - agentCoreGateways: z.array(AgentCoreGatewaySchema), - mcpRuntimeTools: z.array(AgentCoreMcpRuntimeToolSchema).optional(), - unassignedTargets: z.array(AgentCoreGatewayTargetSchema).optional(), - }) - .strict(); - -export type AgentCoreMcpSpec = z.infer; +export interface AgentCoreMcpSpec { + agentCoreGateways: AgentCoreGateway[]; + mcpRuntimeTools?: AgentCoreMcpRuntimeTool[]; + unassignedTargets?: AgentCoreGatewayTarget[]; +}