diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index 0e2f5950..8a5594ab 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -1012,6 +1012,15 @@ exports[`Assets Directory Snapshots > Python framework assets > python/python/au exports[`Assets Directory Snapshots > Python framework assets > python/python/autogen/base/mcp_client/client.py should match snapshot 1`] = ` "from typing import List +{{#if isVpc}} +# VPC mode: external MCP endpoints are not reachable without a NAT gateway. +# Add an AgentCore Gateway with \`agentcore add gateway\`, or configure your own endpoint below. + + +async def get_streamable_http_mcp_tools() -> List: + """No MCP server configured. Add a gateway with \`agentcore add gateway\`.""" + return [] +{{else}} from autogen_ext.tools.mcp import ( StreamableHttpMcpToolAdapter, StreamableHttpServerParams, @@ -1029,6 +1038,7 @@ async def get_streamable_http_mcp_tools() -> List[StreamableHttpMcpToolAdapter]: # to use an MCP server that supports bearer authentication, add headers={"Authorization": f"Bearer {access_token}"} server_params = StreamableHttpServerParams(url=EXAMPLE_MCP_ENDPOINT) return await mcp_server_tools(server_params) +{{/if}} " `; @@ -1792,6 +1802,14 @@ def get_all_gateway_mcp_toolsets() -> list[MCPToolset]: {{/each}} return toolsets {{else}} +{{#if isVpc}} +# VPC mode: external MCP endpoints are not reachable without a NAT gateway. +# Add an AgentCore Gateway with \`agentcore add gateway\`, or configure your own endpoint below. + +def get_streamable_http_mcp_client() -> MCPToolset | None: + """No MCP server configured. Add a gateway with \`agentcore add gateway\`.""" + return None +{{else}} # ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication EXAMPLE_MCP_ENDPOINT = "https://mcp.exa.ai/mcp" @@ -1803,6 +1821,7 @@ def get_streamable_http_mcp_client() -> MCPToolset: connection_params=StreamableHTTPConnectionParams(url=EXAMPLE_MCP_ENDPOINT) ) {{/if}} +{{/if}} " `; @@ -2098,6 +2117,14 @@ def get_all_gateway_mcp_client() -> MultiServerMCPClient | None: return None return MultiServerMCPClient(servers) {{else}} +{{#if isVpc}} +# VPC mode: external MCP endpoints are not reachable without a NAT gateway. +# Add an AgentCore Gateway with \`agentcore add gateway\`, or configure your own endpoint below. + +def get_streamable_http_mcp_client() -> MultiServerMCPClient | None: + """No MCP server configured. Add a gateway with \`agentcore add gateway\`.""" + return None +{{else}} # ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication EXAMPLE_MCP_ENDPOINT = "https://mcp.exa.ai/mcp" @@ -2114,6 +2141,7 @@ def get_streamable_http_mcp_client() -> MultiServerMCPClient: } ) {{/if}} +{{/if}} " `; @@ -2545,6 +2573,14 @@ def get_all_gateway_mcp_servers() -> list[MCPServerStreamableHttp]: {{/each}} return servers {{else}} +{{#if isVpc}} +# VPC mode: external MCP endpoints are not reachable without a NAT gateway. +# Add an AgentCore Gateway with \`agentcore add gateway\`, or configure your own endpoint below. + +def get_streamable_http_mcp_client() -> MCPServerStreamableHttp | None: + """No MCP server configured. Add a gateway with \`agentcore add gateway\`.""" + return None +{{else}} # ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication EXAMPLE_MCP_ENDPOINT = "https://mcp.exa.ai/mcp" @@ -2556,6 +2592,7 @@ def get_streamable_http_mcp_client() -> MCPServerStreamableHttp: name="AgentCore Gateway MCP", params={"url": EXAMPLE_MCP_ENDPOINT} ) {{/if}} +{{/if}} " `; @@ -2879,6 +2916,14 @@ def get_all_gateway_mcp_clients() -> list[MCPClient]: {{/each}} return clients {{else}} +{{#if isVpc}} +# VPC mode: external MCP endpoints are not reachable without a NAT gateway. +# Add an AgentCore Gateway with \`agentcore add gateway\`, or configure your own endpoint below. + +def get_streamable_http_mcp_client() -> MCPClient | None: + """No MCP server configured. Add a gateway with \`agentcore add gateway\`.""" + return None +{{else}} # ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication EXAMPLE_MCP_ENDPOINT = "https://mcp.exa.ai/mcp" @@ -2887,6 +2932,7 @@ def get_streamable_http_mcp_client() -> MCPClient: # to use an MCP server that supports bearer authentication, add headers={"Authorization": f"Bearer {access_token}"} return MCPClient(lambda: streamablehttp_client(EXAMPLE_MCP_ENDPOINT)) {{/if}} +{{/if}} " `; diff --git a/src/assets/python/autogen/base/mcp_client/client.py b/src/assets/python/autogen/base/mcp_client/client.py index 7f4c5c3b..4bdc6a93 100644 --- a/src/assets/python/autogen/base/mcp_client/client.py +++ b/src/assets/python/autogen/base/mcp_client/client.py @@ -1,4 +1,13 @@ from typing import List +{{#if isVpc}} +# VPC mode: external MCP endpoints are not reachable without a NAT gateway. +# Add an AgentCore Gateway with `agentcore add gateway`, or configure your own endpoint below. + + +async def get_streamable_http_mcp_tools() -> List: + """No MCP server configured. Add a gateway with `agentcore add gateway`.""" + return [] +{{else}} from autogen_ext.tools.mcp import ( StreamableHttpMcpToolAdapter, StreamableHttpServerParams, @@ -16,3 +25,4 @@ async def get_streamable_http_mcp_tools() -> List[StreamableHttpMcpToolAdapter]: # to use an MCP server that supports bearer authentication, add headers={"Authorization": f"Bearer {access_token}"} server_params = StreamableHttpServerParams(url=EXAMPLE_MCP_ENDPOINT) return await mcp_server_tools(server_params) +{{/if}} diff --git a/src/assets/python/googleadk/base/mcp_client/client.py b/src/assets/python/googleadk/base/mcp_client/client.py index e6dddd62..df9e0512 100644 --- a/src/assets/python/googleadk/base/mcp_client/client.py +++ b/src/assets/python/googleadk/base/mcp_client/client.py @@ -53,6 +53,14 @@ def get_all_gateway_mcp_toolsets() -> list[MCPToolset]: {{/each}} return toolsets {{else}} +{{#if isVpc}} +# VPC mode: external MCP endpoints are not reachable without a NAT gateway. +# Add an AgentCore Gateway with `agentcore add gateway`, or configure your own endpoint below. + +def get_streamable_http_mcp_client() -> MCPToolset | None: + """No MCP server configured. Add a gateway with `agentcore add gateway`.""" + return None +{{else}} # ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication EXAMPLE_MCP_ENDPOINT = "https://mcp.exa.ai/mcp" @@ -64,3 +72,4 @@ def get_streamable_http_mcp_client() -> MCPToolset: connection_params=StreamableHTTPConnectionParams(url=EXAMPLE_MCP_ENDPOINT) ) {{/if}} +{{/if}} diff --git a/src/assets/python/langchain_langgraph/base/mcp_client/client.py b/src/assets/python/langchain_langgraph/base/mcp_client/client.py index 71b336d2..8fbf92da 100644 --- a/src/assets/python/langchain_langgraph/base/mcp_client/client.py +++ b/src/assets/python/langchain_langgraph/base/mcp_client/client.py @@ -50,6 +50,14 @@ def get_all_gateway_mcp_client() -> MultiServerMCPClient | None: return None return MultiServerMCPClient(servers) {{else}} +{{#if isVpc}} +# VPC mode: external MCP endpoints are not reachable without a NAT gateway. +# Add an AgentCore Gateway with `agentcore add gateway`, or configure your own endpoint below. + +def get_streamable_http_mcp_client() -> MultiServerMCPClient | None: + """No MCP server configured. Add a gateway with `agentcore add gateway`.""" + return None +{{else}} # ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication EXAMPLE_MCP_ENDPOINT = "https://mcp.exa.ai/mcp" @@ -66,3 +74,4 @@ def get_streamable_http_mcp_client() -> MultiServerMCPClient: } ) {{/if}} +{{/if}} diff --git a/src/assets/python/openaiagents/base/mcp_client/client.py b/src/assets/python/openaiagents/base/mcp_client/client.py index 2fe91136..901fe5fa 100644 --- a/src/assets/python/openaiagents/base/mcp_client/client.py +++ b/src/assets/python/openaiagents/base/mcp_client/client.py @@ -52,6 +52,14 @@ def get_all_gateway_mcp_servers() -> list[MCPServerStreamableHttp]: {{/each}} return servers {{else}} +{{#if isVpc}} +# VPC mode: external MCP endpoints are not reachable without a NAT gateway. +# Add an AgentCore Gateway with `agentcore add gateway`, or configure your own endpoint below. + +def get_streamable_http_mcp_client() -> MCPServerStreamableHttp | None: + """No MCP server configured. Add a gateway with `agentcore add gateway`.""" + return None +{{else}} # ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication EXAMPLE_MCP_ENDPOINT = "https://mcp.exa.ai/mcp" @@ -63,3 +71,4 @@ def get_streamable_http_mcp_client() -> MCPServerStreamableHttp: name="AgentCore Gateway MCP", params={"url": EXAMPLE_MCP_ENDPOINT} ) {{/if}} +{{/if}} diff --git a/src/assets/python/strands/base/mcp_client/client.py b/src/assets/python/strands/base/mcp_client/client.py index 01457de2..981c806a 100644 --- a/src/assets/python/strands/base/mcp_client/client.py +++ b/src/assets/python/strands/base/mcp_client/client.py @@ -54,6 +54,14 @@ def get_all_gateway_mcp_clients() -> list[MCPClient]: {{/each}} return clients {{else}} +{{#if isVpc}} +# VPC mode: external MCP endpoints are not reachable without a NAT gateway. +# Add an AgentCore Gateway with `agentcore add gateway`, or configure your own endpoint below. + +def get_streamable_http_mcp_client() -> MCPClient | None: + """No MCP server configured. Add a gateway with `agentcore add gateway`.""" + return None +{{else}} # ExaAI provides information about code through web searches, crawling and code context searches through their platform. Requires no authentication EXAMPLE_MCP_ENDPOINT = "https://mcp.exa.ai/mcp" @@ -62,3 +70,4 @@ def get_streamable_http_mcp_client() -> MCPClient: # to use an MCP server that supports bearer authentication, add headers={"Authorization": f"Bearer {access_token}"} return MCPClient(lambda: streamablehttp_client(EXAMPLE_MCP_ENDPOINT)) {{/if}} +{{/if}} diff --git a/src/cli/commands/add/__tests__/validate.test.ts b/src/cli/commands/add/__tests__/validate.test.ts index 9a24db86..db453485 100644 --- a/src/cli/commands/add/__tests__/validate.test.ts +++ b/src/cli/commands/add/__tests__/validate.test.ts @@ -989,3 +989,80 @@ describe('validate', () => { }); }); }); + +describe('validateAddAgentOptions - VPC validation', () => { + const baseOptions: AddAgentOptions = { + name: 'TestAgent', + type: 'byo', + language: 'Python', + framework: 'Strands', + modelProvider: 'Bedrock', + build: 'CodeZip', + codeLocation: './app/test/', + }; + + it('accepts valid VPC options', () => { + const result = validateAddAgentOptions({ + ...baseOptions, + networkMode: 'VPC', + subnets: 'subnet-12345678', + securityGroups: 'sg-12345678', + }); + expect(result.valid).toBe(true); + }); + + it('accepts PUBLIC network mode without VPC options', () => { + const result = validateAddAgentOptions({ + ...baseOptions, + networkMode: 'PUBLIC', + }); + expect(result.valid).toBe(true); + }); + + it('rejects invalid network mode', () => { + const result = validateAddAgentOptions({ + ...baseOptions, + networkMode: 'INVALID', + }); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid network mode'); + }); + + it('rejects VPC mode without subnets', () => { + const result = validateAddAgentOptions({ + ...baseOptions, + networkMode: 'VPC', + securityGroups: 'sg-12345678', + }); + expect(result.valid).toBe(false); + expect(result.error).toContain('--subnets is required'); + }); + + it('rejects VPC mode without security groups', () => { + const result = validateAddAgentOptions({ + ...baseOptions, + networkMode: 'VPC', + subnets: 'subnet-12345678', + }); + expect(result.valid).toBe(false); + expect(result.error).toContain('--security-groups is required'); + }); + + it('rejects subnets without VPC mode', () => { + const result = validateAddAgentOptions({ + ...baseOptions, + subnets: 'subnet-12345678', + }); + expect(result.valid).toBe(false); + expect(result.error).toContain('only valid with --network-mode VPC'); + }); + + it('rejects security groups without VPC mode', () => { + const result = validateAddAgentOptions({ + ...baseOptions, + securityGroups: 'sg-12345678', + }); + expect(result.valid).toBe(false); + expect(result.error).toContain('only valid with --network-mode VPC'); + }); +}); diff --git a/src/cli/commands/add/types.ts b/src/cli/commands/add/types.ts index 6f39c224..6e0ffab8 100644 --- a/src/cli/commands/add/types.ts +++ b/src/cli/commands/add/types.ts @@ -1,8 +1,9 @@ import type { GatewayAuthorizerType, ModelProvider, SDKFramework, TargetLanguage } from '../../../schema'; import type { MemoryOption } from '../../tui/screens/generate/types'; +import type { VpcOptions } from '../shared/vpc-utils'; // Agent types -export interface AddAgentOptions { +export interface AddAgentOptions extends VpcOptions { name?: string; type?: 'create' | 'byo'; build?: string; diff --git a/src/cli/commands/add/validate.ts b/src/cli/commands/add/validate.ts index 8c77476d..7ee0c4e8 100644 --- a/src/cli/commands/add/validate.ts +++ b/src/cli/commands/add/validate.ts @@ -11,6 +11,7 @@ import { getSupportedModelProviders, matchEnumValue, } from '../../../schema'; +import { validateVpcOptions } from '../shared/vpc-utils'; import type { AddAgentOptions, AddGatewayOptions, @@ -150,6 +151,12 @@ export function validateAddAgentOptions(options: AddAgentOptions): ValidationRes } } + // Validate VPC options + const vpcResult = validateVpcOptions(options); + if (!vpcResult.valid) { + return { valid: false, error: vpcResult.error }; + } + return { valid: true }; } diff --git a/src/cli/commands/create/__tests__/validate.test.ts b/src/cli/commands/create/__tests__/validate.test.ts index 8137f5d7..e465d44e 100644 --- a/src/cli/commands/create/__tests__/validate.test.ts +++ b/src/cli/commands/create/__tests__/validate.test.ts @@ -165,3 +165,118 @@ describe('validateCreateOptions', () => { expect(typeof result.valid).toBe('boolean'); }); }); + +describe('validateCreateOptions - VPC validation', () => { + const cwd = join(tmpdir(), `create-vpc-${randomUUID()}`); + + const baseOptions = { + name: 'TestProject', + language: 'Python', + framework: 'Strands', + modelProvider: 'Bedrock', + memory: 'none', + }; + + it('accepts valid VPC options', () => { + const result = validateCreateOptions( + { + ...baseOptions, + networkMode: 'VPC', + subnets: 'subnet-12345678', + securityGroups: 'sg-12345678', + }, + cwd + ); + expect(result.valid).toBe(true); + }); + + it('accepts PUBLIC network mode without VPC options', () => { + const result = validateCreateOptions( + { + ...baseOptions, + networkMode: 'PUBLIC', + }, + cwd + ); + expect(result.valid).toBe(true); + }); + + it('accepts no network mode (defaults to PUBLIC)', () => { + const result = validateCreateOptions({ ...baseOptions }, cwd); + expect(result.valid).toBe(true); + }); + + it('rejects invalid network mode', () => { + const result = validateCreateOptions( + { + ...baseOptions, + networkMode: 'INVALID', + }, + cwd + ); + expect(result.valid).toBe(false); + expect(result.error).toContain('Invalid network mode'); + }); + + it('rejects VPC mode without subnets', () => { + const result = validateCreateOptions( + { + ...baseOptions, + networkMode: 'VPC', + securityGroups: 'sg-12345678', + }, + cwd + ); + expect(result.valid).toBe(false); + expect(result.error).toContain('--subnets is required'); + }); + + it('rejects VPC mode without security groups', () => { + const result = validateCreateOptions( + { + ...baseOptions, + networkMode: 'VPC', + subnets: 'subnet-12345678', + }, + cwd + ); + expect(result.valid).toBe(false); + expect(result.error).toContain('--security-groups is required'); + }); + + it('rejects subnets without VPC mode', () => { + const result = validateCreateOptions( + { + ...baseOptions, + subnets: 'subnet-12345678', + }, + cwd + ); + expect(result.valid).toBe(false); + expect(result.error).toContain('only valid with --network-mode VPC'); + }); + + it('rejects security groups without VPC mode', () => { + const result = validateCreateOptions( + { + ...baseOptions, + securityGroups: 'sg-12345678', + }, + cwd + ); + expect(result.valid).toBe(false); + expect(result.error).toContain('only valid with --network-mode VPC'); + }); + + it('rejects VPC mode missing both subnets and security groups', () => { + const result = validateCreateOptions( + { + ...baseOptions, + networkMode: 'VPC', + }, + cwd + ); + expect(result.valid).toBe(false); + expect(result.error).toContain('--subnets is required'); + }); +}); diff --git a/src/cli/commands/create/action.ts b/src/cli/commands/create/action.ts index c99f69dc..ac1c06fa 100644 --- a/src/cli/commands/create/action.ts +++ b/src/cli/commands/create/action.ts @@ -4,6 +4,7 @@ import type { BuildType, DeployedState, ModelProvider, + NetworkMode, SDKFramework, TargetLanguage, } from '../../../schema'; @@ -120,6 +121,9 @@ export interface CreateWithAgentOptions { modelProvider: ModelProvider; apiKey?: string; memory: MemoryOption; + networkMode?: NetworkMode; + subnets?: string[]; + securityGroups?: string[]; skipGit?: boolean; skipPythonSetup?: boolean; onProgress?: ProgressCallback; @@ -135,6 +139,9 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P modelProvider, apiKey, memory, + networkMode, + subnets, + securityGroups, skipGit, skipPythonSetup, onProgress, @@ -172,6 +179,9 @@ export async function createProjectWithAgent(options: CreateWithAgentOptions): P apiKey, memory, language, + networkMode, + subnets, + securityGroups, }; // Resolve credential strategy FIRST (new project has no existing credentials) diff --git a/src/cli/commands/create/command.tsx b/src/cli/commands/create/command.tsx index ada69dd8..eada2793 100644 --- a/src/cli/commands/create/command.tsx +++ b/src/cli/commands/create/command.tsx @@ -1,8 +1,9 @@ import { getWorkingDirectory } from '../../../lib'; -import type { BuildType, ModelProvider, SDKFramework, TargetLanguage } from '../../../schema'; +import type { BuildType, ModelProvider, NetworkMode, SDKFramework, TargetLanguage } from '../../../schema'; import { getErrorMessage } from '../../errors'; import { COMMAND_DESCRIPTIONS } from '../../tui/copy'; import { CreateScreen } from '../../tui/screens/create'; +import { parseCommaSeparatedList } from '../shared/vpc-utils'; import { type ProgressCallback, createProject, createProjectWithAgent, getDryRunInfo } from './action'; import type { CreateOptions } from './types'; import { validateCreateOptions } from './validate'; @@ -120,6 +121,9 @@ async function handleCreateCLI(options: CreateOptions): Promise { modelProvider: options.modelProvider as ModelProvider, apiKey: options.apiKey, memory: options.memory as 'none' | 'shortTerm' | 'longAndShortTerm', + networkMode: options.networkMode as NetworkMode | undefined, + subnets: parseCommaSeparatedList(options.subnets), + securityGroups: parseCommaSeparatedList(options.securityGroups), skipGit: options.skipGit, skipPythonSetup: options.skipPythonSetup, onProgress, @@ -152,6 +156,9 @@ export const registerCreate = (program: Command) => { .option('--model-provider ', 'Model provider (Bedrock, Anthropic, OpenAI, Gemini) [non-interactive]') .option('--api-key ', 'API key for non-Bedrock providers [non-interactive]') .option('--memory