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
4 changes: 2 additions & 2 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Build and Test

on:
push:
branches: ['main']
branches: ['main', 'feat/gateway-integration']
pull_request:
branches: ['main']
branches: ['main', 'feat/gateway-integration']

permissions:
contents: read
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ name: CodeQL

on:
push:
branches: ['main']
branches: ['main', 'feat/gateway-integration']
pull_request:
branches: ['main']
branches: ['main', 'feat/gateway-integration']
pull_request_target:
branches: ['main']
branches: ['main', 'feat/gateway-integration']

# Cancel in-progress runs when a new commit is pushed
concurrency:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
description: 'AWS region for deployment'
default: 'us-east-1'
pull_request_target:
branches: [main]
branches: [main, feat/gateway-integration]

permissions:
id-token: write # OIDC — lets GitHub assume an AWS IAM role via short-lived token (no stored keys)
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Quality and Safety Checks

on:
push:
branches: ['main']
branches: ['main', 'feat/gateway-integration']
pull_request:
branches: ['main']
branches: ['main', 'feat/gateway-integration']

permissions:
contents: read
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr-size.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ name: PR Size Check and Label
# Safe because this workflow only reads PR metadata — it never checks out untrusted code.
on:
pull_request_target:
branches: [main]
branches: [main, feat/gateway-integration]

jobs:
label-size:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr-title.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Validate PR Title

on:
pull_request_target:
branches: [main]
branches: [main, feat/gateway-integration]
types: [opened, edited, synchronize, reopened]

jobs:
Expand Down
4 changes: 2 additions & 2 deletions src/cli/operations/mcp/create-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export async function getExistingToolNames(): Promise<string[]> {
// Gateway targets
for (const gateway of mcpSpec.agentCoreGateways) {
for (const target of gateway.targets) {
for (const toolDef of target.toolDefinitions) {
for (const toolDef of target.toolDefinitions ?? []) {
toolNames.push(toolDef.name);
}
}
Expand Down Expand Up @@ -256,7 +256,7 @@ export async function createToolFromWizard(config: AddMcpToolConfig): Promise<Cr
// Check for duplicate tool names
for (const toolDef of toolDefs) {
for (const existingTarget of gateway.targets) {
if (existingTarget.toolDefinitions.some(t => t.name === toolDef.name)) {
if ((existingTarget.toolDefinitions ?? []).some(t => t.name === toolDef.name)) {
throw new Error(`Tool "${toolDef.name}" already exists in gateway "${gateway.name}".`);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/cli/operations/remove/remove-mcp-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export async function previewRemoveMcpTool(tool: RemovableMcpTool): Promise<Remo
}

// Tool definitions in mcp-defs
for (const toolDef of target.toolDefinitions) {
for (const toolDef of target.toolDefinitions ?? []) {
if (mcpDefs.tools[toolDef.name]) {
summary.push(`Removing tool definition: ${toolDef.name}`);
}
Expand Down Expand Up @@ -179,7 +179,7 @@ function computeRemovedToolMcpDefs(
const gateway = mcpSpec.agentCoreGateways.find(g => g.name === tool.gatewayName);
const target = gateway?.targets.find(t => t.name === tool.name);
if (target) {
for (const toolDef of target.toolDefinitions) {
for (const toolDef of target.toolDefinitions ?? []) {
toolNamesToRemove.push(toolDef.name);
}
}
Expand Down
1 change: 1 addition & 0 deletions src/cli/tui/screens/mcp/useAddGatewayWizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useCallback, useMemo, useState } from 'react';
/** Maps authorizer type to the next step after authorizer selection */
const AUTHORIZER_NEXT_STEP: Record<GatewayAuthorizerType, AddGatewayStep> = {
NONE: 'agents',
AWS_IAM: 'agents',
CUSTOM_JWT: 'jwt-config',
};

Expand Down
28 changes: 27 additions & 1 deletion src/schema/schemas/__tests__/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,11 @@ describe('AgentCoreGatewayTargetSchema', () => {
name: 'myTarget',
targetType: 'lambda',
toolDefinitions: [validToolDef],
compute: {
host: 'Lambda',
implementation: { language: 'Python', path: 'tools', handler: 'h' },
pythonVersion: 'PYTHON_3_12',
},
});
expect(result.success).toBe(true);
});
Expand All @@ -270,6 +275,11 @@ describe('AgentCoreGatewayTargetSchema', () => {
name: 'myTarget',
targetType: 'lambda',
toolDefinitions: [],
compute: {
host: 'Lambda',
implementation: { language: 'Python', path: 'tools', handler: 'h' },
pythonVersion: 'PYTHON_3_12',
},
});
expect(result.success).toBe(false);
});
Expand Down Expand Up @@ -303,6 +313,11 @@ describe('AgentCoreGatewaySchema', () => {
name: 'target1',
targetType: 'lambda',
toolDefinitions: [validToolDef],
compute: {
host: 'Lambda',
implementation: { language: 'Python', path: 'tools', handler: 'h' },
pythonVersion: 'PYTHON_3_12',
},
},
],
};
Expand Down Expand Up @@ -387,7 +402,18 @@ describe('AgentCoreMcpSpecSchema', () => {
agentCoreGateways: [
{
name: 'gw1',
targets: [{ name: 't1', targetType: 'lambda', toolDefinitions: [validToolDef] }],
targets: [
{
name: 't1',
targetType: 'lambda',
toolDefinitions: [validToolDef],
compute: {
host: 'Lambda',
implementation: { language: 'Python', path: 'tools', handler: 'h' },
pythonVersion: 'PYTHON_3_12',
},
},
],
},
],
});
Expand Down
29 changes: 24 additions & 5 deletions src/schema/schemas/agentcore-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,6 @@ export type Memory = z.infer<typeof MemorySchema>;
// Credential Schema
// ============================================================================

export const CredentialTypeSchema = z.literal('ApiKeyCredentialProvider');
export type CredentialType = z.infer<typeof CredentialTypeSchema>;

export const CredentialNameSchema = z
.string()
.min(3, 'Credential name must be at least 3 characters')
Expand All @@ -83,11 +80,33 @@ export const CredentialNameSchema = z
'Must contain only alphanumeric characters, underscores, dots, and hyphens (3-255 chars)'
);

export const CredentialSchema = z.object({
type: CredentialTypeSchema,
export const CredentialTypeSchema = z.enum(['ApiKeyCredentialProvider', 'OAuthCredentialProvider']);
export type CredentialType = z.infer<typeof CredentialTypeSchema>;

export const ApiKeyCredentialSchema = z.object({
type: z.literal('ApiKeyCredentialProvider'),
name: CredentialNameSchema,
});

export type ApiKeyCredential = z.infer<typeof ApiKeyCredentialSchema>;

export const OAuthCredentialSchema = z.object({
type: z.literal('OAuthCredentialProvider'),
name: CredentialNameSchema,
/** OIDC discovery URL for the OAuth provider */
discoveryUrl: z.string().url(),
/** Scopes this credential provider supports */
scopes: z.array(z.string()).optional(),
/** Credential provider vendor type */
vendor: z.string().default('CustomOauth2'),
/** Whether this credential was auto-created by the CLI (e.g., for CUSTOM_JWT inbound auth) */
managed: z.boolean().optional(),
});

export type OAuthCredential = z.infer<typeof OAuthCredentialSchema>;

export const CredentialSchema = z.discriminatedUnion('type', [ApiKeyCredentialSchema, OAuthCredentialSchema]);

export type Credential = z.infer<typeof CredentialSchema>;

// ============================================================================
Expand Down
55 changes: 52 additions & 3 deletions src/schema/schemas/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type GatewayTargetType = z.infer<typeof GatewayTargetTypeSchema>;
// Gateway Authorization Schemas
// ============================================================================

export const GatewayAuthorizerTypeSchema = z.enum(['NONE', 'CUSTOM_JWT']);
export const GatewayAuthorizerTypeSchema = z.enum(['NONE', 'AWS_IAM', 'CUSTOM_JWT']);
export type GatewayAuthorizerType = z.infer<typeof GatewayAuthorizerTypeSchema>;

/** OIDC well-known configuration endpoint suffix (per OpenID Connect Discovery 1.0 spec) */
Expand Down Expand Up @@ -44,6 +44,7 @@ export const CustomJwtAuthorizerConfigSchema = z.object({
allowedAudience: z.array(z.string().min(1)),
/** List of allowed client IDs */
allowedClients: z.array(z.string().min(1)).min(1),
allowedScopes: z.array(z.string().min(1)).optional(),
});

export type CustomJwtAuthorizerConfig = z.infer<typeof CustomJwtAuthorizerConfigSchema>;
Expand All @@ -57,6 +58,19 @@ export const GatewayAuthorizerConfigSchema = z.object({

export type GatewayAuthorizerConfig = z.infer<typeof GatewayAuthorizerConfigSchema>;

export const OutboundAuthTypeSchema = z.enum(['OAUTH', 'API_KEY', 'NONE']);
export type OutboundAuthType = z.infer<typeof OutboundAuthTypeSchema>;

export const OutboundAuthSchema = z
.object({
type: OutboundAuthTypeSchema.default('NONE'),
credentialName: z.string().min(1).optional(),
scopes: z.array(z.string()).optional(),
})
.strict();

export type OutboundAuth = z.infer<typeof OutboundAuthSchema>;

export const McpImplLanguageSchema = z.enum(['TypeScript', 'Python']);
export type McpImplementationLanguage = z.infer<typeof McpImplLanguageSchema>;

Expand Down Expand Up @@ -262,10 +276,45 @@ export const AgentCoreGatewayTargetSchema = z
.object({
name: z.string().min(1),
targetType: GatewayTargetTypeSchema,
toolDefinitions: z.array(ToolDefinitionSchema).min(1),
/** Tool definitions. Required for Lambda targets. Optional for MCP Server (discovered via tools/list). */
toolDefinitions: z.array(ToolDefinitionSchema).optional(),
/** Compute configuration. Required for Lambda/Runtime scaffold targets. */
compute: ToolComputeConfigSchema.optional(),
/** MCP Server endpoint URL. Required for external MCP Server targets. */
endpoint: z.string().url().optional(),
/** Outbound auth configuration for the target. */
outboundAuth: OutboundAuthSchema.optional(),
})
.strict();
.strict()
.superRefine((data, ctx) => {
if (data.targetType === 'mcpServer' && !data.compute && !data.endpoint) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'MCP Server targets require either an endpoint URL or compute configuration.',
});
}
if (data.targetType === 'lambda' && !data.compute) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Lambda targets require compute configuration.',
path: ['compute'],
});
}
if (data.targetType === 'lambda' && (!data.toolDefinitions || data.toolDefinitions.length === 0)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Lambda targets require at least one tool definition.',
path: ['toolDefinitions'],
});
}
if (data.outboundAuth && data.outboundAuth.type !== 'NONE' && !data.outboundAuth.credentialName) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `${data.outboundAuth.type} outbound auth requires a credentialName.`,
path: ['outboundAuth', 'credentialName'],
});
}
});

export type AgentCoreGatewayTarget = z.infer<typeof AgentCoreGatewayTargetSchema>;

Expand Down
Loading