Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 30 additions & 29 deletions src/cli/commands/add/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,36 @@ export async function validateAddGatewayTargetOptions(options: AddGatewayTargetO
};
}

// API Gateway targets: validate early and return (skip outbound auth validation)
if (mappedType === 'apiGateway') {
if (!options.restApiId) {
return { valid: false, error: '--rest-api-id is required for api-gateway type' };
}
if (!options.stage) {
return { valid: false, error: '--stage is required for api-gateway type' };
}
if (options.endpoint) {
return { valid: false, error: '--endpoint is not applicable for api-gateway type' };
}
if (options.host) {
return { valid: false, error: '--host is not applicable for api-gateway type' };
}
if (options.language && options.language !== 'Other') {
return { valid: false, error: '--language is not applicable for api-gateway type' };
}
if (options.outboundAuthType) {
return { valid: false, error: '--outbound-auth is not applicable for api-gateway type' };
}
if (options.credentialName) {
return { valid: false, error: '--credential-name is not applicable for api-gateway type' };
}
if (options.oauthClientId || options.oauthClientSecret || options.oauthDiscoveryUrl || options.oauthScopes) {
return { valid: false, error: 'OAuth options are not applicable for api-gateway type' };
}
options.language = 'Other';
return { valid: true };
}

// Validate outbound auth configuration
if (options.outboundAuthType && options.outboundAuthType !== 'NONE') {
const hasInlineOAuth = !!(options.oauthClientId ?? options.oauthClientSecret ?? options.oauthDiscoveryUrl);
Expand Down Expand Up @@ -309,35 +339,6 @@ export async function validateAddGatewayTargetOptions(options: AddGatewayTargetO
}
}

if (mappedType === 'apiGateway') {
if (!options.restApiId) {
return { valid: false, error: '--rest-api-id is required for api-gateway type' };
}
if (!options.stage) {
return { valid: false, error: '--stage is required for api-gateway type' };
}
if (options.endpoint) {
return { valid: false, error: '--endpoint is not applicable for api-gateway type' };
}
if (options.host) {
return { valid: false, error: '--host is not applicable for api-gateway type' };
}
if (options.language && options.language !== 'Other') {
return { valid: false, error: '--language is not applicable for api-gateway type' };
}
if (options.outboundAuthType) {
return { valid: false, error: '--outbound-auth is not applicable for api-gateway type' };
}
if (options.credentialName) {
return { valid: false, error: '--credential-name is not applicable for api-gateway type' };
}
if (options.oauthClientId || options.oauthClientSecret || options.oauthDiscoveryUrl || options.oauthScopes) {
return { valid: false, error: 'OAuth options are not applicable for api-gateway type' };
}
options.language = 'Other';
return { valid: true };
}

if (mappedType === 'mcpServer') {
if (options.host) {
return { valid: false, error: '--host is not applicable for MCP server targets' };
Expand Down
82 changes: 27 additions & 55 deletions src/cli/primitives/GatewayTargetPrimitive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
AgentCoreCliMcpDefs,
AgentCoreGatewayTarget,
AgentCoreMcpSpec,
ApiGatewayHttpMethod,
DirectoryPath,
FilePath,
} from '../../schema';
Expand All @@ -13,7 +14,7 @@ import { getErrorMessage } from '../errors';
import type { RemovableGatewayTarget } from '../operations/remove/remove-gateway-target';
import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types';
import { getTemplateToolDefinitions, renderGatewayTargetTemplate } from '../templates/GatewayTargetRenderer';
import type { AddGatewayTargetConfig } from '../tui/screens/mcp/types';
import type { ApiGatewayTargetConfig, GatewayTargetWizardState, McpServerTargetConfig } from '../tui/screens/mcp/types';
import { DEFAULT_HANDLER, DEFAULT_NODE_VERSION, DEFAULT_PYTHON_VERSION } from '../tui/screens/mcp/types';
import { BasePrimitive } from './BasePrimitive';
import { SOURCE_CODE_NOTE } from './constants';
Expand Down Expand Up @@ -279,26 +280,19 @@ export class GatewayTargetPrimitive extends BasePrimitive<AddGatewayTargetOption

// Handle API Gateway targets (no code generation)
if (cliOptions.type === 'apiGateway') {
const config: AddGatewayTargetConfig = {
name: cliOptions.name!,
description: cliOptions.description ?? `API Gateway target for ${cliOptions.name!}`,
sourcePath: '',
language: 'Other',
host: 'AgentCoreRuntime',
const config: ApiGatewayTargetConfig = {
targetType: 'apiGateway',
toolDefinition: {
name: cliOptions.name!,
description: cliOptions.description ?? `API Gateway target for ${cliOptions.name!}`,
inputSchema: { type: 'object' },
},
gateway: cliOptions.gateway,
restApiId: cliOptions.restApiId,
stage: cliOptions.stage,
name: cliOptions.name!,
gateway: cliOptions.gateway!,
restApiId: cliOptions.restApiId!,
stage: cliOptions.stage!,
toolFilters: cliOptions.toolFilterPath
? [
{
filterPath: cliOptions.toolFilterPath,
methods: cliOptions.toolFilterMethods?.split(',').map(m => m.trim()) ?? ['GET'],
methods: (cliOptions.toolFilterMethods?.split(',').map(m => m.trim()) ?? [
'GET',
]) as ApiGatewayHttpMethod[],
},
]
: undefined,
Expand All @@ -315,20 +309,17 @@ export class GatewayTargetPrimitive extends BasePrimitive<AddGatewayTargetOption

// Handle MCP server targets (existing endpoint, no code generation)
if (cliOptions.type === 'mcpServer' && cliOptions.endpoint) {
const config: AddGatewayTargetConfig = {
const config: McpServerTargetConfig = {
targetType: 'mcpServer',
name: cliOptions.name!,
description: cliOptions.description ?? `Tool for ${cliOptions.name!}`,
sourcePath: '',
language: cliOptions.language ?? 'Other',
host: 'AgentCoreRuntime',
targetType: 'mcpServer',
endpoint: cliOptions.endpoint,
gateway: cliOptions.gateway!,
toolDefinition: {
name: cliOptions.name!,
description: cliOptions.description ?? `Tool for ${cliOptions.name!}`,
inputSchema: { type: 'object' },
},
gateway: cliOptions.gateway,
endpoint: cliOptions.endpoint,
...(cliOptions.outboundAuthType
? {
outboundAuth: {
Expand Down Expand Up @@ -448,20 +439,14 @@ export class GatewayTargetPrimitive extends BasePrimitive<AddGatewayTargetOption
* Create an external gateway target that connects to an existing MCP server endpoint.
* Unlike `add()` which scaffolds new code, this registers an existing endpoint URL.
*/
async createExternalGatewayTarget(
config: AddGatewayTargetConfig
): Promise<{ toolName: string; projectPath: string }> {
if (!config.endpoint) {
throw new Error('Endpoint URL is required for external MCP server targets.');
}

async createExternalGatewayTarget(config: McpServerTargetConfig): Promise<{ toolName: string; projectPath: string }> {
const mcpSpec: AgentCoreMcpSpec = this.configIO.configExists('mcp')
? await this.configIO.readMcpSpec()
: { agentCoreGateways: [] };

const target: AgentCoreGatewayTarget = {
name: config.name,
targetType: config.targetType ?? 'mcpServer',
targetType: 'mcpServer',
endpoint: config.endpoint,
toolDefinitions: [config.toolDefinition],
...(config.outboundAuth && { outboundAuth: config.outboundAuth }),
Expand Down Expand Up @@ -494,17 +479,7 @@ export class GatewayTargetPrimitive extends BasePrimitive<AddGatewayTargetOption
* Create an API Gateway target that connects to an existing Amazon API Gateway REST API.
* Unlike `add()` which scaffolds new code, this registers an existing REST API.
*/
async createApiGatewayTarget(config: AddGatewayTargetConfig): Promise<{ toolName: string }> {
if (!config.restApiId) {
throw new Error('REST API ID is required for API Gateway targets.');
}
if (!config.stage) {
throw new Error('Stage is required for API Gateway targets.');
}
if (!config.gateway) {
throw new Error('Gateway name is required.');
}

async createApiGatewayTarget(config: ApiGatewayTargetConfig): Promise<{ toolName: string }> {
const mcpSpec: AgentCoreMcpSpec = this.configIO.configExists('mcp')
? await this.configIO.readMcpSpec()
: { agentCoreGateways: [] };
Expand All @@ -529,10 +504,7 @@ export class GatewayTargetPrimitive extends BasePrimitive<AddGatewayTargetOption
restApiId: config.restApiId,
stage: config.stage,
apiGatewayToolConfiguration: {
toolFilters: (config.toolFilters ?? [{ filterPath: '/*', methods: ['GET'] }]) as {
filterPath: string;
methods: ('GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS')[];
}[],
toolFilters: config.toolFilters ?? [{ filterPath: '/*', methods: ['GET'] }],
},
},
};
Expand All @@ -547,7 +519,7 @@ export class GatewayTargetPrimitive extends BasePrimitive<AddGatewayTargetOption
// Private helpers
// ═══════════════════════════════════════════════════════════════════

private buildGatewayTargetConfig(options: AddGatewayTargetOptions): AddGatewayTargetConfig {
private buildGatewayTargetConfig(options: AddGatewayTargetOptions): GatewayTargetWizardState {
const sourcePath = `${APP_DIR}/${MCP_APP_SUBDIR}/${options.name}`;
const description = options.description ?? `Tool for ${options.name}`;
return {
Expand All @@ -566,16 +538,16 @@ export class GatewayTargetPrimitive extends BasePrimitive<AddGatewayTargetOption
}

private async createToolFromWizard(
config: AddGatewayTargetConfig
config: GatewayTargetWizardState
): Promise<{ mcpDefsPath: string; toolName: string; projectPath: string }> {
this.validateGatewayTargetLanguage(config.language);
this.validateGatewayTargetLanguage(config.language!);

const mcpSpec: AgentCoreMcpSpec = this.configIO.configExists('mcp')
? await this.configIO.readMcpSpec()
: { agentCoreGateways: [] };

const toolDefs =
config.host === 'Lambda' ? getTemplateToolDefinitions(config.name, config.host) : [config.toolDefinition];
config.host === 'Lambda' ? getTemplateToolDefinitions(config.name, config.host) : [config.toolDefinition!];

for (const toolDef of toolDefs) {
ToolDefinitionSchema.parse(toolDef);
Expand Down Expand Up @@ -615,7 +587,7 @@ export class GatewayTargetPrimitive extends BasePrimitive<AddGatewayTargetOption
? {
host: 'Lambda',
implementation: {
path: config.sourcePath,
path: config.sourcePath!,
language: config.language,
handler: DEFAULT_HANDLER,
},
Expand All @@ -626,7 +598,7 @@ export class GatewayTargetPrimitive extends BasePrimitive<AddGatewayTargetOption
: {
host: 'AgentCoreRuntime',
implementation: {
path: config.sourcePath,
path: config.sourcePath!,
language: 'Python',
handler: 'server.py:main',
},
Expand All @@ -635,7 +607,7 @@ export class GatewayTargetPrimitive extends BasePrimitive<AddGatewayTargetOption
pythonVersion: DEFAULT_PYTHON_VERSION,
name: config.name,
entrypoint: 'server.py:main' as FilePath,
codeLocation: config.sourcePath as DirectoryPath,
codeLocation: config.sourcePath! as DirectoryPath,
networkMode: 'PUBLIC',
},
},
Expand Down Expand Up @@ -663,10 +635,10 @@ export class GatewayTargetPrimitive extends BasePrimitive<AddGatewayTargetOption
// Render gateway target project template
const configRoot = requireConfigRoot();
const projectRoot = dirname(configRoot);
const absoluteSourcePath = join(projectRoot, config.sourcePath);
const absoluteSourcePath = join(projectRoot, config.sourcePath!);
await renderGatewayTargetTemplate(config.name, absoluteSourcePath, config.language, config.host);

return { mcpDefsPath, toolName: config.name, projectPath: config.sourcePath };
return { mcpDefsPath, toolName: config.name, projectPath: config.sourcePath! };
}

private validateGatewayTargetLanguage(language: string): asserts language is 'Python' | 'TypeScript' | 'Other' {
Expand Down
18 changes: 11 additions & 7 deletions src/cli/tui/components/ResourceGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@
);
}

export function getTargetDisplayText(target: AgentCoreGatewayTarget): string {

Check warning on line 101 in src/cli/tui/components/ResourceGraph.tsx

View workflow job for this annotation

GitHub Actions / lint

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
if (target.targetType === 'mcpServer' && target.endpoint) return target.endpoint;
if (target.targetType === 'apiGateway' && target.apiGateway)
return `${target.apiGateway.restApiId}/${target.apiGateway.stage}`;
return target.name;
}

export function ResourceGraph({ project, mcp, agentName, resourceStatuses }: ResourceGraphProps) {
const allAgents = project.agents ?? [];
const agents = agentName ? allAgents.filter(a => a.name === agentName) : allAgents;
Expand Down Expand Up @@ -244,13 +251,13 @@
identifier={rsEntry?.identifier}
/>
{targets.map(target => {
const displayText =
target.targetType === 'mcpServer' && target.endpoint ? target.endpoint : target.name;
const displayText = getTargetDisplayText(target);
return (
<Text key={target.name}>
{' '}
<Text color="cyan">{ICONS.tool}</Text> {displayText}
{target.targetType === 'mcpServer' && target.endpoint && (
{(target.targetType === 'apiGateway' ||
(target.targetType === 'mcpServer' && target.endpoint)) && (
<Text color="gray"> [{target.targetType}]</Text>
)}
</Text>
Expand Down Expand Up @@ -282,10 +289,7 @@
<Box flexDirection="column">
<SectionHeader>⚠ Unassigned Targets</SectionHeader>
{unassignedTargets.map((target, idx) => {
const displayText =
target.targetType === 'mcpServer' && target.endpoint
? target.endpoint
: (target.name ?? `Target ${idx + 1}`);
const displayText = getTargetDisplayText(target);
return <ResourceRow key={idx} icon="⚠" color="yellow" name={displayText} detail={target.targetType} />;
})}
</Box>
Expand Down
33 changes: 32 additions & 1 deletion src/cli/tui/components/__tests__/ResourceGraph.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { AgentCoreMcpSpec, AgentCoreProjectSpec } from '../../../../schema/index.js';
import type { ResourceStatusEntry } from '../../../commands/status/action.js';
import { ResourceGraph } from '../ResourceGraph.js';
import { ResourceGraph, getTargetDisplayText } from '../ResourceGraph.js';
import { render } from 'ink-testing-library';
import React from 'react';
import { describe, expect, it } from 'vitest';
Expand Down Expand Up @@ -326,3 +326,34 @@ describe('ResourceGraph', () => {
});
});
});

describe('getTargetDisplayText', () => {
it('returns endpoint for mcpServer with endpoint', () => {
const target = { name: 'my-tool', targetType: 'mcpServer', endpoint: 'https://example.com/mcp' } as any;
expect(getTargetDisplayText(target)).toBe('https://example.com/mcp');
});

it('returns restApiId/stage for apiGateway', () => {
const target = {
name: 'my-api',
targetType: 'apiGateway',
apiGateway: { restApiId: 'abc123', stage: 'prod' },
} as any;
expect(getTargetDisplayText(target)).toBe('abc123/prod');
});

it('returns name for mcpServer without endpoint', () => {
const target = { name: 'my-tool', targetType: 'mcpServer' } as any;
expect(getTargetDisplayText(target)).toBe('my-tool');
});

it('returns name for unknown target type', () => {
const target = { name: 'my-tool', targetType: 'lambda' } as any;
expect(getTargetDisplayText(target)).toBe('my-tool');
});

it('returns name for apiGateway without apiGateway config', () => {
const target = { name: 'my-api', targetType: 'apiGateway' } as any;
expect(getTargetDisplayText(target)).toBe('my-api');
});
});
2 changes: 1 addition & 1 deletion src/cli/tui/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export { useExitHandler } from './useExitHandler';
export { useListNavigation } from './useListNavigation';
export { useMultiSelectNavigation } from './useMultiSelectNavigation';
export { useResponsive } from './useResponsive';
export { useAvailableAgents, useCreateGateway, useCreateGatewayTarget, useExistingGateways } from './useCreateMcp';
export { useAvailableAgents, useCreateGateway, useExistingGateways } from './useCreateMcp';
export { useDevServer } from './useDevServer';
export { useProject } from './useProject';
export type { UseProjectResult, ProjectContext } from './useProject';
Expand Down
Loading
Loading