Skip to content

Commit 37252b3

Browse files
committed
feat: add external MCP server target support and unassigned targets
1 parent e832292 commit 37252b3

File tree

10 files changed

+239
-39
lines changed

10 files changed

+239
-39
lines changed

src/cli/commands/add/actions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ export interface ValidatedAddGatewayOptions {
6666
export interface ValidatedAddGatewayTargetOptions {
6767
name: string;
6868
description?: string;
69+
type?: string;
70+
source?: 'existing-endpoint' | 'create-new';
71+
endpoint?: string;
6972
language: 'Python' | 'TypeScript' | 'Other';
7073
exposure: 'mcp-runtime' | 'behind-gateway';
7174
agents?: string;
@@ -304,6 +307,8 @@ function buildGatewayTargetConfig(options: ValidatedAddGatewayTargetOptions): Ad
304307
sourcePath,
305308
language: options.language,
306309
exposure: options.exposure,
310+
source: options.source,
311+
endpoint: options.endpoint,
307312
host: options.exposure === 'mcp-runtime' ? 'AgentCoreRuntime' : options.host!,
308313
toolDefinition: {
309314
name: options.name,

src/cli/commands/add/command.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,9 @@ export function registerAdd(program: Command) {
262262
.description('Add a gateway target to the project')
263263
.option('--name <name>', 'Tool name')
264264
.option('--description <desc>', 'Tool description')
265+
.option('--type <type>', 'Target type: mcpServer or lambda')
266+
.option('--source <source>', 'Source: existing-endpoint or create-new')
267+
.option('--endpoint <url>', 'MCP server endpoint URL')
265268
.option('--language <lang>', 'Language: Python or TypeScript')
266269
.option('--exposure <mode>', 'Exposure mode: mcp-runtime or behind-gateway')
267270
.option('--agents <names>', 'Comma-separated agent names (for mcp-runtime)')

src/cli/commands/add/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ export interface AddGatewayResult {
4545
export interface AddGatewayTargetOptions {
4646
name?: string;
4747
description?: string;
48+
type?: string;
49+
source?: string;
50+
endpoint?: string;
4851
language?: 'Python' | 'TypeScript' | 'Other';
4952
exposure?: 'mcp-runtime' | 'behind-gateway';
5053
agents?: string;

src/cli/commands/add/validate.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,31 @@ export async function validateAddGatewayTargetOptions(options: AddGatewayTargetO
189189
return { valid: false, error: '--name is required' };
190190
}
191191

192+
if (options.type && options.type !== 'mcpServer' && options.type !== 'lambda') {
193+
return { valid: false, error: 'Invalid type. Valid options: mcpServer, lambda' };
194+
}
195+
196+
if (options.source && options.source !== 'existing-endpoint' && options.source !== 'create-new') {
197+
return { valid: false, error: 'Invalid source. Valid options: existing-endpoint, create-new' };
198+
}
199+
200+
if (options.source === 'existing-endpoint') {
201+
if (!options.endpoint) {
202+
return { valid: false, error: '--endpoint is required when source is existing-endpoint' };
203+
}
204+
205+
try {
206+
const url = new URL(options.endpoint);
207+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
208+
return { valid: false, error: 'Endpoint must use http:// or https:// protocol' };
209+
}
210+
} catch {
211+
return { valid: false, error: 'Endpoint must be a valid URL (e.g. https://example.com/mcp)' };
212+
}
213+
214+
return { valid: true };
215+
}
216+
192217
if (!options.language) {
193218
return { valid: false, error: '--language is required' };
194219
}

src/cli/operations/mcp/create-mcp.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,57 @@ async function validateCredentialName(credentialName: string): Promise<void> {
198198
}
199199
}
200200

201+
/**
202+
* Create an external MCP server target (existing endpoint).
203+
*/
204+
export async function createExternalGatewayTarget(config: AddGatewayTargetConfig): Promise<CreateToolResult> {
205+
if (!config.endpoint) {
206+
throw new Error('Endpoint URL is required for external MCP server targets.');
207+
}
208+
209+
const configIO = new ConfigIO();
210+
const mcpSpec: AgentCoreMcpSpec = configIO.configExists('mcp')
211+
? await configIO.readMcpSpec()
212+
: { agentCoreGateways: [], unassignedTargets: [] };
213+
214+
const target: AgentCoreGatewayTarget = {
215+
name: config.name,
216+
targetType: 'mcpServer',
217+
endpoint: config.endpoint,
218+
toolDefinitions: [config.toolDefinition],
219+
...(config.outboundAuth && { outboundAuth: config.outboundAuth }),
220+
};
221+
222+
if (config.gateway && config.gateway !== 'skip-for-now') {
223+
// Assign to specific gateway
224+
const gateway = mcpSpec.agentCoreGateways.find(g => g.name === config.gateway);
225+
if (!gateway) {
226+
throw new Error(`Gateway "${config.gateway}" not found.`);
227+
}
228+
229+
// Check for duplicate target name
230+
if (gateway.targets.some(t => t.name === config.name)) {
231+
throw new Error(`Target "${config.name}" already exists in gateway "${gateway.name}".`);
232+
}
233+
234+
gateway.targets.push(target);
235+
} else {
236+
// Add to unassigned targets
237+
mcpSpec.unassignedTargets ??= [];
238+
239+
// Check for duplicate target name in unassigned targets
240+
if (mcpSpec.unassignedTargets.some((t: AgentCoreGatewayTarget) => t.name === config.name)) {
241+
throw new Error(`Unassigned target "${config.name}" already exists.`);
242+
}
243+
244+
mcpSpec.unassignedTargets.push(target);
245+
}
246+
247+
await configIO.writeMcpSpec(mcpSpec);
248+
249+
return { mcpDefsPath: '', toolName: config.name, projectPath: '' };
250+
}
251+
201252
/**
202253
* Create an MCP tool (MCP runtime or behind gateway).
203254
*/

src/cli/tui/screens/mcp/AddGatewayTargetFlow.tsx

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createExternalGatewayTarget } from '../../../operations/mcp/create-mcp';
12
import { ErrorPrompt, Panel, Screen, TextInput, WizardSelect } from '../../components';
23
import type { SelectableItem } from '../../components';
34
import { HELP_TEXT } from '../../constants';
@@ -114,14 +115,25 @@ export function AddGatewayTargetFlow({
114115
loading: true,
115116
loadingMessage: 'Creating MCP tool...',
116117
});
117-
void createTool(config).then(result => {
118-
if (result.ok) {
119-
const { toolName, projectPath } = result.result;
120-
setFlow({ name: 'create-success', toolName, projectPath });
121-
return;
122-
}
123-
setFlow({ name: 'error', message: result.error });
124-
});
118+
119+
if (config.source === 'existing-endpoint') {
120+
void createExternalGatewayTarget(config)
121+
.then((result: { toolName: string; projectPath: string }) => {
122+
setFlow({ name: 'create-success', toolName: result.toolName, projectPath: result.projectPath });
123+
})
124+
.catch((err: Error) => {
125+
setFlow({ name: 'error', message: err instanceof Error ? err.message : 'Unknown error' });
126+
});
127+
} else {
128+
void createTool(config).then(result => {
129+
if (result.ok) {
130+
const { toolName, projectPath } = result.result;
131+
setFlow({ name: 'create-success', toolName, projectPath });
132+
return;
133+
}
134+
setFlow({ name: 'error', message: result.error });
135+
});
136+
}
125137
},
126138
[createTool]
127139
);

src/cli/tui/screens/mcp/AddGatewayTargetScreen.tsx

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ import { HELP_TEXT } from '../../constants';
1313
import { useListNavigation, useMultiSelectNavigation } from '../../hooks';
1414
import { generateUniqueName } from '../../utils';
1515
import type { AddGatewayTargetConfig, ComputeHost, ExposureMode, TargetLanguage } from './types';
16-
import { COMPUTE_HOST_OPTIONS, EXPOSURE_MODE_OPTIONS, MCP_TOOL_STEP_LABELS, TARGET_LANGUAGE_OPTIONS } from './types';
16+
import {
17+
COMPUTE_HOST_OPTIONS,
18+
EXPOSURE_MODE_OPTIONS,
19+
MCP_TOOL_STEP_LABELS,
20+
SOURCE_OPTIONS,
21+
TARGET_LANGUAGE_OPTIONS,
22+
} from './types';
1723
import { useAddGatewayTargetWizard } from './useAddGatewayTargetWizard';
1824
import { Box, Text } from 'ink';
1925
import React, { useMemo } from 'react';
@@ -35,6 +41,11 @@ export function AddGatewayTargetScreen({
3541
}: AddGatewayTargetScreenProps) {
3642
const wizard = useAddGatewayTargetWizard(existingGateways, existingAgents);
3743

44+
const sourceItems: SelectableItem[] = useMemo(
45+
() => SOURCE_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })),
46+
[]
47+
);
48+
3849
const languageItems: SelectableItem[] = useMemo(
3950
() => TARGET_LANGUAGE_OPTIONS.map(o => ({ id: o.id, title: o.title, description: o.description })),
4051
[]
@@ -52,7 +63,10 @@ export function AddGatewayTargetScreen({
5263
);
5364

5465
const gatewayItems: SelectableItem[] = useMemo(
55-
() => existingGateways.map(g => ({ id: g, title: g })),
66+
() => [
67+
...existingGateways.map(g => ({ id: g, title: g })),
68+
{ id: 'skip-for-now', title: 'Skip for now', description: 'Create unassigned target' },
69+
],
5670
[existingGateways]
5771
);
5872

@@ -63,16 +77,24 @@ export function AddGatewayTargetScreen({
6377

6478
const agentItems: SelectableItem[] = useMemo(() => existingAgents.map(a => ({ id: a, title: a })), [existingAgents]);
6579

80+
const isSourceStep = wizard.step === 'source';
6681
const isLanguageStep = wizard.step === 'language';
6782
const isExposureStep = wizard.step === 'exposure';
6883
const isAgentsStep = wizard.step === 'agents';
6984
const isGatewayStep = wizard.step === 'gateway';
7085
const isHostStep = wizard.step === 'host';
71-
const isTextStep = wizard.step === 'name';
86+
const isTextStep = wizard.step === 'name' || wizard.step === 'endpoint';
7287
const isConfirmStep = wizard.step === 'confirm';
7388
const noGatewaysAvailable = isGatewayStep && existingGateways.length === 0;
7489
const noAgentsAvailable = isAgentsStep && existingAgents.length === 0;
7590

91+
const sourceNav = useListNavigation({
92+
items: sourceItems,
93+
onSelect: item => wizard.setSource(item.id as 'existing-endpoint' | 'create-new'),
94+
onExit: () => wizard.goBack(),
95+
isActive: isSourceStep,
96+
});
97+
7698
const languageNav = useListNavigation({
7799
items: languageItems,
78100
onSelect: item => wizard.setLanguage(item.id as TargetLanguage),
@@ -132,6 +154,15 @@ export function AddGatewayTargetScreen({
132154
return (
133155
<Screen title="Add MCP Tool" onExit={onExit} helpText={helpText} headerContent={headerContent}>
134156
<Panel>
157+
{isSourceStep && (
158+
<WizardSelect
159+
title="Select source"
160+
description="How would you like to create this MCP tool?"
161+
items={sourceItems}
162+
selectedIndex={sourceNav.selectedIndex}
163+
/>
164+
)}
165+
135166
{isLanguageStep && (
136167
<WizardSelect title="Select language" items={languageItems} selectedIndex={languageNav.selectedIndex} />
137168
)}
@@ -180,27 +211,43 @@ export function AddGatewayTargetScreen({
180211
{isTextStep && (
181212
<TextInput
182213
key={wizard.step}
183-
prompt={MCP_TOOL_STEP_LABELS[wizard.step]}
184-
initialValue={generateUniqueName('mytool', existingToolNames)}
185-
onSubmit={wizard.setName}
214+
prompt={wizard.step === 'endpoint' ? 'MCP server endpoint URL' : MCP_TOOL_STEP_LABELS[wizard.step]}
215+
initialValue={
216+
wizard.step === 'endpoint' ? 'https://example.com/mcp' : generateUniqueName('mytool', existingToolNames)
217+
}
218+
onSubmit={wizard.step === 'endpoint' ? wizard.setEndpoint : wizard.setName}
186219
onCancel={() => (wizard.currentIndex === 0 ? onExit() : wizard.goBack())}
187-
schema={ToolNameSchema}
188-
customValidation={value => !existingToolNames.includes(value) || 'Tool name already exists'}
220+
schema={wizard.step === 'name' ? ToolNameSchema : undefined}
221+
customValidation={
222+
wizard.step === 'name'
223+
? value => !existingToolNames.includes(value) || 'Tool name already exists'
224+
: undefined
225+
}
189226
/>
190227
)}
191228

192229
{isConfirmStep && (
193230
<ConfirmReview
194231
fields={[
195232
{ label: 'Name', value: wizard.config.name },
196-
{ label: 'Language', value: wizard.config.language },
197-
{ label: 'Exposure', value: isMcpRuntime ? 'MCP Runtime' : 'Behind Gateway' },
233+
{
234+
label: 'Source',
235+
value: wizard.config.source === 'existing-endpoint' ? 'Existing endpoint' : 'Create new',
236+
},
237+
...(wizard.config.endpoint ? [{ label: 'Endpoint', value: wizard.config.endpoint }] : []),
238+
...(wizard.config.source === 'create-new' ? [{ label: 'Language', value: wizard.config.language }] : []),
239+
...(wizard.config.source === 'create-new'
240+
? [{ label: 'Exposure', value: isMcpRuntime ? 'MCP Runtime' : 'Behind Gateway' }]
241+
: []),
198242
...(isMcpRuntime && wizard.config.selectedAgents.length > 0
199243
? [{ label: 'Agents', value: wizard.config.selectedAgents.join(', ') }]
200244
: []),
201245
...(!isMcpRuntime && wizard.config.gateway ? [{ label: 'Gateway', value: wizard.config.gateway }] : []),
202-
{ label: 'Host', value: wizard.config.host },
203-
{ label: 'Source', value: wizard.config.sourcePath },
246+
...(!isMcpRuntime && !wizard.config.gateway
247+
? [{ label: 'Gateway', value: '(none - assign later)' }]
248+
: []),
249+
...(wizard.config.source === 'create-new' ? [{ label: 'Host', value: wizard.config.host }] : []),
250+
...(wizard.config.source === 'create-new' ? [{ label: 'Source', value: wizard.config.sourcePath }] : []),
204251
]}
205252
/>
206253
)}

src/cli/tui/screens/mcp/types.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,16 @@ export type ComputeHost = 'Lambda' | 'AgentCoreRuntime';
4747
* - host: Select compute host (only if behind-gateway)
4848
* - confirm: Review and confirm
4949
*/
50-
export type AddGatewayTargetStep = 'name' | 'language' | 'exposure' | 'agents' | 'gateway' | 'host' | 'confirm';
50+
export type AddGatewayTargetStep =
51+
| 'name'
52+
| 'source'
53+
| 'endpoint'
54+
| 'language'
55+
| 'exposure'
56+
| 'agents'
57+
| 'gateway'
58+
| 'host'
59+
| 'confirm';
5160

5261
export type TargetLanguage = 'Python' | 'TypeScript' | 'Other';
5362

@@ -57,6 +66,10 @@ export interface AddGatewayTargetConfig {
5766
sourcePath: string;
5867
language: TargetLanguage;
5968
exposure: ExposureMode;
69+
/** Source type for external endpoints */
70+
source?: 'existing-endpoint' | 'create-new';
71+
/** External endpoint URL */
72+
endpoint?: string;
6073
/** Gateway name (only when exposure = behind-gateway) */
6174
gateway?: string;
6275
/** Compute host (AgentCoreRuntime for mcp-runtime, Lambda or AgentCoreRuntime for behind-gateway) */
@@ -75,6 +88,8 @@ export interface AddGatewayTargetConfig {
7588

7689
export const MCP_TOOL_STEP_LABELS: Record<AddGatewayTargetStep, string> = {
7790
name: 'Name',
91+
source: 'Source',
92+
endpoint: 'Endpoint',
7893
language: 'Language',
7994
exposure: 'Exposure',
8095
agents: 'Agents',
@@ -93,6 +108,11 @@ export const AUTHORIZER_TYPE_OPTIONS = [
93108
{ id: 'NONE', title: 'None', description: 'No authorization required — gateway is publicly accessible' },
94109
] as const;
95110

111+
export const SOURCE_OPTIONS = [
112+
{ id: 'existing-endpoint', title: 'Existing endpoint', description: 'Connect to an existing MCP server' },
113+
{ id: 'create-new', title: 'Create new', description: 'Scaffold a new MCP server' },
114+
] as const;
115+
96116
export const TARGET_LANGUAGE_OPTIONS = [
97117
{ id: 'Python', title: 'Python', description: 'FastMCP Python server' },
98118
{ id: 'TypeScript', title: 'TypeScript', description: 'MCP TypeScript server' },

0 commit comments

Comments
 (0)