Skip to content

Commit bb9cf2b

Browse files
authored
fix(tool): resolve HTTP gateways in add tool --gateway (#1658 follow-up) (#1693)
`agentcore add tool --gateway <name>` only searched `resources.mcp.gateways` for the named gateway, missing HTTP gateways stored under `resources.gateways`. This is the same root cause as #1658 (fixed in #1663 for PolicyPrimitive), but the fix was not applied to `tool-action.ts`. Replace the manual lookup with the `findDeployedGateway()` helper introduced in #1663, which correctly merges both gateway storage locations and searches across all deployment targets. Fixes the scenario where a user deploys an HTTP gateway (e.g. with CUSTOM_JWT auth) and then cannot reference it by name in `add tool`.
1 parent 0f115e7 commit bb9cf2b

2 files changed

Lines changed: 330 additions & 7 deletions

File tree

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
import type { HarnessSpec } from '../../../../schema';
2+
import { handleAddTool } from '../tool-action.js';
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4+
5+
const mockReadHarnessSpec = vi.fn();
6+
const mockWriteHarnessSpec = vi.fn();
7+
const mockReadDeployedState = vi.fn();
8+
9+
vi.mock('../../../../lib/index.js', () => ({
10+
ConfigIO: class {
11+
readHarnessSpec = mockReadHarnessSpec;
12+
writeHarnessSpec = mockWriteHarnessSpec;
13+
readDeployedState = mockReadDeployedState;
14+
},
15+
}));
16+
17+
function makeHarnessSpec(overrides: Partial<HarnessSpec> = {}): HarnessSpec {
18+
return {
19+
name: 'TestHarness',
20+
model: { provider: 'bedrock', modelId: 'anthropic.claude-3-5-sonnet-20240620-v1:0' },
21+
tools: [],
22+
skills: [],
23+
...overrides,
24+
} as HarnessSpec;
25+
}
26+
27+
describe('handleAddTool — gateway resolution', () => {
28+
beforeEach(() => {
29+
mockReadHarnessSpec.mockReset();
30+
mockWriteHarnessSpec.mockReset();
31+
mockReadDeployedState.mockReset();
32+
mockReadHarnessSpec.mockResolvedValue(makeHarnessSpec());
33+
});
34+
35+
afterEach(() => {
36+
vi.resetAllMocks();
37+
});
38+
39+
it('resolves an MCP gateway stored under resources.mcp.gateways', async () => {
40+
mockReadDeployedState.mockResolvedValue({
41+
targets: {
42+
default: {
43+
resources: {
44+
mcp: {
45+
gateways: {
46+
'my-gateway': {
47+
gatewayId: 'mcp-gw-id',
48+
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123456789012:gateway/mcp-gw-id',
49+
},
50+
},
51+
},
52+
},
53+
},
54+
},
55+
});
56+
57+
const result = await handleAddTool({
58+
harness: 'TestHarness',
59+
type: 'agentcore_gateway',
60+
name: 'my-tool',
61+
gateway: 'my-gateway',
62+
outboundAuth: 'awsIam',
63+
});
64+
65+
expect(result.success).toBe(true);
66+
expect(mockWriteHarnessSpec).toHaveBeenCalledWith(
67+
'TestHarness',
68+
expect.objectContaining({
69+
tools: [
70+
expect.objectContaining({
71+
type: 'agentcore_gateway',
72+
name: 'my-tool',
73+
config: {
74+
agentCoreGateway: {
75+
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123456789012:gateway/mcp-gw-id',
76+
outboundAuth: { awsIam: {} },
77+
},
78+
},
79+
}),
80+
],
81+
})
82+
);
83+
});
84+
85+
it('resolves an HTTP gateway stored under resources.gateways (not under mcp)', async () => {
86+
mockReadDeployedState.mockResolvedValue({
87+
targets: {
88+
default: {
89+
resources: {
90+
gateways: {
91+
'my-gateway': {
92+
gatewayId: 'http-gw-id',
93+
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123456789012:gateway/http-gw-id',
94+
},
95+
},
96+
},
97+
},
98+
},
99+
});
100+
101+
const result = await handleAddTool({
102+
harness: 'TestHarness',
103+
type: 'agentcore_gateway',
104+
name: 'my-tool',
105+
gateway: 'my-gateway',
106+
outboundAuth: 'none',
107+
});
108+
109+
expect(result.success).toBe(true);
110+
expect(mockWriteHarnessSpec).toHaveBeenCalledWith(
111+
'TestHarness',
112+
expect.objectContaining({
113+
tools: [
114+
expect.objectContaining({
115+
config: {
116+
agentCoreGateway: {
117+
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123456789012:gateway/http-gw-id',
118+
outboundAuth: { none: {} },
119+
},
120+
},
121+
}),
122+
],
123+
})
124+
);
125+
});
126+
127+
it('resolves a gateway from a non-first target', async () => {
128+
mockReadDeployedState.mockResolvedValue({
129+
targets: {
130+
staging: { resources: {} },
131+
production: {
132+
resources: {
133+
gateways: {
134+
'my-gateway': {
135+
gatewayId: 'prod-gw-id',
136+
gatewayArn: 'arn:aws:bedrock-agentcore:us-east-1:123456789012:gateway/prod-gw-id',
137+
},
138+
},
139+
},
140+
},
141+
},
142+
});
143+
144+
const result = await handleAddTool({
145+
harness: 'TestHarness',
146+
type: 'agentcore_gateway',
147+
name: 'my-tool',
148+
gateway: 'my-gateway',
149+
outboundAuth: 'awsIam',
150+
});
151+
152+
expect(result.success).toBe(true);
153+
expect(mockWriteHarnessSpec).toHaveBeenCalledWith(
154+
'TestHarness',
155+
expect.objectContaining({
156+
tools: [
157+
expect.objectContaining({
158+
config: {
159+
agentCoreGateway: {
160+
gatewayArn: 'arn:aws:bedrock-agentcore:us-east-1:123456789012:gateway/prod-gw-id',
161+
outboundAuth: { awsIam: {} },
162+
},
163+
},
164+
}),
165+
],
166+
})
167+
);
168+
});
169+
170+
it('finds gateway when both MCP and HTTP gateways coexist', async () => {
171+
mockReadDeployedState.mockResolvedValue({
172+
targets: {
173+
default: {
174+
resources: {
175+
mcp: {
176+
gateways: {
177+
'mcp-gw': { gatewayId: 'mcp-id', gatewayArn: 'arn:mcp' },
178+
},
179+
},
180+
gateways: {
181+
'http-gw': { gatewayId: 'http-id', gatewayArn: 'arn:http' },
182+
},
183+
},
184+
},
185+
},
186+
});
187+
188+
const result = await handleAddTool({
189+
harness: 'TestHarness',
190+
type: 'agentcore_gateway',
191+
name: 'my-tool',
192+
gateway: 'http-gw',
193+
outboundAuth: 'awsIam',
194+
});
195+
196+
expect(result.success).toBe(true);
197+
expect(mockWriteHarnessSpec).toHaveBeenCalledWith(
198+
'TestHarness',
199+
expect.objectContaining({
200+
tools: [
201+
expect.objectContaining({
202+
config: { agentCoreGateway: { gatewayArn: 'arn:http', outboundAuth: { awsIam: {} } } },
203+
}),
204+
],
205+
})
206+
);
207+
});
208+
209+
it('returns error when gateway name is not found in any location', async () => {
210+
mockReadDeployedState.mockResolvedValue({
211+
targets: {
212+
default: {
213+
resources: {
214+
mcp: { gateways: { 'other-gw': { gatewayId: 'x', gatewayArn: 'arn:x' } } },
215+
},
216+
},
217+
},
218+
});
219+
220+
const result = await handleAddTool({
221+
harness: 'TestHarness',
222+
type: 'agentcore_gateway',
223+
name: 'my-tool',
224+
gateway: 'nonexistent',
225+
});
226+
227+
expect(result.success).toBe(false);
228+
expect(result.error).toContain("Gateway 'nonexistent' not found in deployed state");
229+
});
230+
231+
it('returns error when deployed state cannot be read', async () => {
232+
mockReadDeployedState.mockRejectedValue(new Error('File not found'));
233+
234+
const result = await handleAddTool({
235+
harness: 'TestHarness',
236+
type: 'agentcore_gateway',
237+
name: 'my-tool',
238+
gateway: 'my-gateway',
239+
});
240+
241+
expect(result.success).toBe(false);
242+
expect(result.error).toContain('Could not read deployed state');
243+
});
244+
245+
it('skips gateway resolution when --gateway-arn is provided directly', async () => {
246+
const result = await handleAddTool({
247+
harness: 'TestHarness',
248+
type: 'agentcore_gateway',
249+
name: 'my-tool',
250+
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123456789012:gateway/direct-arn',
251+
outboundAuth: 'awsIam',
252+
});
253+
254+
expect(result.success).toBe(true);
255+
expect(mockReadDeployedState).not.toHaveBeenCalled();
256+
expect(mockWriteHarnessSpec).toHaveBeenCalledWith(
257+
'TestHarness',
258+
expect.objectContaining({
259+
tools: [
260+
expect.objectContaining({
261+
config: {
262+
agentCoreGateway: {
263+
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123456789012:gateway/direct-arn',
264+
outboundAuth: { awsIam: {} },
265+
},
266+
},
267+
}),
268+
],
269+
})
270+
);
271+
});
272+
273+
it('resolves gateway with oauth outbound auth', async () => {
274+
mockReadDeployedState.mockResolvedValue({
275+
targets: {
276+
default: {
277+
resources: {
278+
gateways: {
279+
'my-gateway': {
280+
gatewayId: 'gw-id',
281+
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123456789012:gateway/gw-id',
282+
},
283+
},
284+
},
285+
},
286+
},
287+
});
288+
289+
const result = await handleAddTool({
290+
harness: 'TestHarness',
291+
type: 'agentcore_gateway',
292+
name: 'my-tool',
293+
gateway: 'my-gateway',
294+
outboundAuth: 'oauth',
295+
providerArn:
296+
'arn:aws:bedrock-agentcore:us-west-2:123456789012:token-vault/default/oauth2credentialprovider/my-cred',
297+
scopes: 'openid,profile',
298+
grantType: 'CLIENT_CREDENTIALS',
299+
});
300+
301+
expect(result.success).toBe(true);
302+
expect(mockWriteHarnessSpec).toHaveBeenCalledWith(
303+
'TestHarness',
304+
expect.objectContaining({
305+
tools: [
306+
expect.objectContaining({
307+
config: {
308+
agentCoreGateway: {
309+
gatewayArn: 'arn:aws:bedrock-agentcore:us-west-2:123456789012:gateway/gw-id',
310+
outboundAuth: {
311+
oauth: {
312+
providerArn:
313+
'arn:aws:bedrock-agentcore:us-west-2:123456789012:token-vault/default/oauth2credentialprovider/my-cred',
314+
scopes: ['openid', 'profile'],
315+
grantType: 'CLIENT_CREDENTIALS',
316+
},
317+
},
318+
},
319+
},
320+
}),
321+
],
322+
})
323+
);
324+
});
325+
});

src/cli/commands/add/tool-action.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ConfigIO } from '../../../lib';
22
import type { HarnessGatewayOutboundAuth, HarnessSpec } from '../../../schema';
33
import type { HarnessToolType } from '../../../schema/schemas/primitives/harness';
4+
import { findDeployedGateway } from '../../primitives/deployed-gateways';
45
import { readFileSync } from 'fs';
56

67
export interface AddToolOptions {
@@ -152,17 +153,14 @@ export async function handleAddTool(options: AddToolOptions): Promise<AddToolRes
152153

153154
const configIO = new ConfigIO();
154155

155-
// Resolve --gateway (project name) to ARN from deployed-state
156+
// Resolve --gateway (project name) to ARN from deployed-state.
157+
// Uses findDeployedGateway which searches both `resources.mcp.gateways` (MCP protocol)
158+
// and `resources.gateways` (HTTP / protocolType: "None") across all targets.
156159
let resolvedGatewayArn = options.gatewayArn;
157160
if (toolType === 'agentcore_gateway' && options.gateway && !resolvedGatewayArn) {
158161
try {
159162
const deployedState = await configIO.readDeployedState();
160-
const targetNames = Object.keys(deployedState.targets);
161-
if (targetNames.length === 0) {
162-
return { success: false, error: 'No deployed targets found. Deploy the gateway first.' };
163-
}
164-
const targetState = deployedState.targets[targetNames[0]!];
165-
const gatewayState = targetState?.resources?.mcp?.gateways?.[options.gateway];
163+
const gatewayState = findDeployedGateway(deployedState, options.gateway);
166164
if (!gatewayState) {
167165
return {
168166
success: false,

0 commit comments

Comments
 (0)