From 35cce5aa7aabf081c3f20f91bc1cdcdcda1bebae Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 25 Feb 2026 17:59:17 -0500 Subject: [PATCH 1/3] feat: wire existing-endpoint flow for gateway targets --- src/cli/commands/add/actions.ts | 10 +++++++++- src/cli/commands/add/validate.ts | 3 +++ src/cli/tui/screens/mcp/useAddGatewayTargetWizard.ts | 11 +++-------- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/cli/commands/add/actions.ts b/src/cli/commands/add/actions.ts index 6d28bfc7..0e2543f9 100644 --- a/src/cli/commands/add/actions.ts +++ b/src/cli/commands/add/actions.ts @@ -23,7 +23,11 @@ import { createCredential, resolveCredentialStrategy, } from '../../operations/identity/create-identity'; -import { createGatewayFromWizard, createToolFromWizard } from '../../operations/mcp/create-mcp'; +import { + createExternalGatewayTarget, + createGatewayFromWizard, + createToolFromWizard, +} from '../../operations/mcp/create-mcp'; import { createMemory } from '../../operations/memory/create-memory'; import { createRenderer } from '../../templates'; import type { MemoryOption } from '../../tui/screens/generate/types'; @@ -334,6 +338,10 @@ export async function handleAddGatewayTarget( } const config = buildGatewayTargetConfig(options); + if (config.source === 'existing-endpoint') { + const result = await createExternalGatewayTarget(config); + return { success: true, toolName: result.toolName }; + } const result = await createToolFromWizard(config); return { success: true, toolName: result.toolName, sourcePath: result.projectPath }; } catch (err) { diff --git a/src/cli/commands/add/validate.ts b/src/cli/commands/add/validate.ts index d7cbc802..2731ed83 100644 --- a/src/cli/commands/add/validate.ts +++ b/src/cli/commands/add/validate.ts @@ -198,6 +198,9 @@ export async function validateAddGatewayTargetOptions(options: AddGatewayTargetO } if (options.source === 'existing-endpoint') { + if (options.host) { + return { valid: false, error: '--host is not applicable for existing endpoint targets' }; + } if (!options.endpoint) { return { valid: false, error: '--endpoint is required when source is existing-endpoint' }; } diff --git a/src/cli/tui/screens/mcp/useAddGatewayTargetWizard.ts b/src/cli/tui/screens/mcp/useAddGatewayTargetWizard.ts index 29aa54ac..57437f82 100644 --- a/src/cli/tui/screens/mcp/useAddGatewayTargetWizard.ts +++ b/src/cli/tui/screens/mcp/useAddGatewayTargetWizard.ts @@ -7,13 +7,13 @@ import { useCallback, useMemo, useState } from 'react'; /** * Dynamic steps based on source. * - Existing endpoint: name → source → endpoint → gateway → confirm - * - Create new: name → source → language → gateway → host → confirm + * - Create new: name → source → language → gateway → confirm */ function getSteps(source?: 'existing-endpoint' | 'create-new'): AddGatewayTargetStep[] { if (source === 'existing-endpoint') { return ['name', 'source', 'endpoint', 'gateway', 'confirm']; } - return ['name', 'source', 'language', 'gateway', 'host', 'confirm']; + return ['name', 'source', 'language', 'gateway', 'confirm']; } function deriveToolDefinition(name: string): ToolDefinition { @@ -91,13 +91,8 @@ export function useAddGatewayTargetWizard(existingGateways: string[] = []) { const setGateway = useCallback((gateway: string) => { setConfig(c => { - const isExternal = c.source === 'existing-endpoint'; const isSkipped = gateway === SKIP_FOR_NOW; - if (isExternal || isSkipped) { - setStep('confirm'); - } else { - setStep('host'); - } + setStep('confirm'); return { ...c, gateway: isSkipped ? undefined : gateway }; }); }, []); From a6ad2268370321688098a2aa285faab2b2ab1f4e Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 25 Feb 2026 18:10:49 -0500 Subject: [PATCH 2/3] test: add routing and validation tests for existing-endpoint flow --- .../commands/add/__tests__/actions.test.ts | 34 ++++++++++++++++++- .../commands/add/__tests__/validate.test.ts | 12 +++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/cli/commands/add/__tests__/actions.test.ts b/src/cli/commands/add/__tests__/actions.test.ts index 852523bc..0fffde89 100644 --- a/src/cli/commands/add/__tests__/actions.test.ts +++ b/src/cli/commands/add/__tests__/actions.test.ts @@ -1,6 +1,15 @@ import { buildGatewayTargetConfig } from '../actions.js'; import type { ValidatedAddGatewayTargetOptions } from '../actions.js'; -import { describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const mockCreateToolFromWizard = vi.fn().mockResolvedValue({ toolName: 'test', projectPath: '/tmp' }); +const mockCreateExternalGatewayTarget = vi.fn().mockResolvedValue({ toolName: 'test', projectPath: '' }); + +vi.mock('../../../operations/mcp/create-mcp', () => ({ + createToolFromWizard: (...args: unknown[]) => mockCreateToolFromWizard(...args), + createExternalGatewayTarget: (...args: unknown[]) => mockCreateExternalGatewayTarget(...args), + createGatewayFromWizard: vi.fn(), +})); describe('buildGatewayTargetConfig', () => { it('maps name, gateway, language correctly', () => { @@ -66,3 +75,26 @@ describe('buildGatewayTargetConfig', () => { expect(config.outboundAuth).toBeUndefined(); }); }); + +// Dynamic import to pick up mocks +const { handleAddGatewayTarget } = await import('../actions.js'); + +describe('handleAddGatewayTarget', () => { + afterEach(() => vi.clearAllMocks()); + + it('routes existing-endpoint to createExternalGatewayTarget', async () => { + const options: ValidatedAddGatewayTargetOptions = { + name: 'test-tool', + language: 'Other', + host: 'Lambda', + source: 'existing-endpoint', + endpoint: 'https://example.com/mcp', + gateway: 'my-gw', + }; + + await handleAddGatewayTarget(options); + + expect(mockCreateExternalGatewayTarget).toHaveBeenCalledOnce(); + expect(mockCreateToolFromWizard).not.toHaveBeenCalled(); + }); +}); diff --git a/src/cli/commands/add/__tests__/validate.test.ts b/src/cli/commands/add/__tests__/validate.test.ts index e9a7992a..40f5dfec 100644 --- a/src/cli/commands/add/__tests__/validate.test.ts +++ b/src/cli/commands/add/__tests__/validate.test.ts @@ -422,6 +422,18 @@ describe('validate', () => { expect(result.valid).toBe(false); expect(result.error).toContain('--credential-name is required'); }); + + it('rejects --host with existing-endpoint', async () => { + const options: AddGatewayTargetOptions = { + name: 'test-tool', + source: 'existing-endpoint', + endpoint: 'https://example.com/mcp', + host: 'Lambda', + }; + const result = await validateAddGatewayTargetOptions(options); + expect(result.valid).toBe(false); + expect(result.error).toBe('--host is not applicable for existing endpoint targets'); + }); }); describe('validateAddMemoryOptions', () => { From d3fda7047d6753e965af484c2586de1153aa5420 Mon Sep 17 00:00:00 2001 From: Aidan Daly Date: Wed, 25 Feb 2026 18:42:51 -0500 Subject: [PATCH 3/3] refactor: remove source/language/host steps, existing-endpoint is the only flow --- .../screens/mcp/AddGatewayTargetScreen.tsx | 71 +------------------ .../screens/mcp/useAddGatewayTargetWizard.ts | 55 +++----------- 2 files changed, 12 insertions(+), 114 deletions(-) diff --git a/src/cli/tui/screens/mcp/AddGatewayTargetScreen.tsx b/src/cli/tui/screens/mcp/AddGatewayTargetScreen.tsx index f8a2522e..0c811d43 100644 --- a/src/cli/tui/screens/mcp/AddGatewayTargetScreen.tsx +++ b/src/cli/tui/screens/mcp/AddGatewayTargetScreen.tsx @@ -4,14 +4,8 @@ import type { SelectableItem } from '../../components'; import { HELP_TEXT } from '../../constants'; import { useListNavigation } from '../../hooks'; import { generateUniqueName } from '../../utils'; -import type { AddGatewayTargetConfig, ComputeHost, TargetLanguage } from './types'; -import { - COMPUTE_HOST_OPTIONS, - MCP_TOOL_STEP_LABELS, - SKIP_FOR_NOW, - SOURCE_OPTIONS, - TARGET_LANGUAGE_OPTIONS, -} from './types'; +import type { AddGatewayTargetConfig } from './types'; +import { MCP_TOOL_STEP_LABELS, SKIP_FOR_NOW } from './types'; import { useAddGatewayTargetWizard } from './useAddGatewayTargetWizard'; import { Box, Text } from 'ink'; import React, { useMemo } from 'react'; @@ -31,16 +25,6 @@ export function AddGatewayTargetScreen({ }: AddGatewayTargetScreenProps) { const wizard = useAddGatewayTargetWizard(existingGateways); - const sourceItems: SelectableItem[] = useMemo( - () => SOURCE_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })), - [] - ); - - const languageItems: SelectableItem[] = useMemo( - () => TARGET_LANGUAGE_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })), - [] - ); - const gatewayItems: SelectableItem[] = useMemo( () => [ ...existingGateways.map(g => ({ id: g, title: g })), @@ -49,33 +33,11 @@ export function AddGatewayTargetScreen({ [existingGateways] ); - const hostItems: SelectableItem[] = useMemo( - () => COMPUTE_HOST_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })), - [] - ); - - const isSourceStep = wizard.step === 'source'; - const isLanguageStep = wizard.step === 'language'; const isGatewayStep = wizard.step === 'gateway'; - const isHostStep = wizard.step === 'host'; const isTextStep = wizard.step === 'name' || wizard.step === 'endpoint'; const isConfirmStep = wizard.step === 'confirm'; const noGatewaysAvailable = isGatewayStep && existingGateways.length === 0; - const sourceNav = useListNavigation({ - items: sourceItems, - onSelect: item => wizard.setSource(item.id as 'existing-endpoint' | 'create-new'), - onExit: () => wizard.goBack(), - isActive: isSourceStep, - }); - - const languageNav = useListNavigation({ - items: languageItems, - onSelect: item => wizard.setLanguage(item.id as TargetLanguage), - onExit: () => onExit(), - isActive: isLanguageStep, - }); - const gatewayNav = useListNavigation({ items: gatewayItems, onSelect: item => wizard.setGateway(item.id), @@ -83,13 +45,6 @@ export function AddGatewayTargetScreen({ isActive: isGatewayStep && !noGatewaysAvailable, }); - const hostNav = useListNavigation({ - items: hostItems, - onSelect: item => wizard.setHost(item.id as ComputeHost), - onExit: () => wizard.goBack(), - isActive: isHostStep, - }); - useListNavigation({ items: [{ id: 'confirm', title: 'Confirm' }], onSelect: () => onComplete(wizard.config), @@ -108,19 +63,6 @@ export function AddGatewayTargetScreen({ return ( - {isSourceStep && ( - - )} - - {isLanguageStep && ( - - )} - {isGatewayStep && !noGatewaysAvailable && ( } - {isHostStep && ( - - )} - {isTextStep && ( (getDefaultConfig); const [step, setStep] = useState('name'); - const steps = useMemo(() => getSteps(config.source), [config.source]); + const steps = useMemo(() => getSteps(), []); const currentIndex = steps.indexOf(step); const goBack = useCallback(() => { - // Recalculate steps in case source changed - const currentSteps = getSteps(config.source); + const currentSteps = getSteps(); const idx = currentSteps.indexOf(step); const prevStep = currentSteps[idx - 1]; if (prevStep) setStep(prevStep); - }, [config.source, step]); + }, [step]); const setName = useCallback((name: string) => { setConfig(c => ({ @@ -58,19 +54,7 @@ export function useAddGatewayTargetWizard(existingGateways: string[] = []) { sourcePath: `${APP_DIR}/${MCP_APP_SUBDIR}/${name}`, toolDefinition: deriveToolDefinition(name), })); - setStep('source'); - }, []); - - const setSource = useCallback((source: 'existing-endpoint' | 'create-new') => { - setConfig(c => ({ - ...c, - source, - })); - if (source === 'existing-endpoint') { - setStep('endpoint'); - } else { - setStep('language'); - } + setStep('endpoint'); }, []); const setEndpoint = useCallback((endpoint: string) => { @@ -81,14 +65,6 @@ export function useAddGatewayTargetWizard(existingGateways: string[] = []) { setStep('gateway'); }, []); - const setLanguage = useCallback((language: TargetLanguage) => { - setConfig(c => ({ - ...c, - language, - })); - setStep('gateway'); - }, []); - const setGateway = useCallback((gateway: string) => { setConfig(c => { const isSkipped = gateway === SKIP_FOR_NOW; @@ -97,14 +73,6 @@ export function useAddGatewayTargetWizard(existingGateways: string[] = []) { }); }, []); - const setHost = useCallback((host: ComputeHost) => { - setConfig(c => ({ - ...c, - host, - })); - setStep('confirm'); - }, []); - const reset = useCallback(() => { setConfig(getDefaultConfig()); setStep('name'); @@ -118,11 +86,8 @@ export function useAddGatewayTargetWizard(existingGateways: string[] = []) { existingGateways, goBack, setName, - setSource, setEndpoint, - setLanguage, setGateway, - setHost, reset, }; }