Skip to content

Commit 0122b98

Browse files
committed
Handling Custom MCP connections consumption
1 parent 0ccc701 commit 0122b98

File tree

5 files changed

+838
-6
lines changed

5 files changed

+838
-6
lines changed

libs/designer-v2/src/lib/ui/panel/connectionsPanel/createConnection/createConnection.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -405,12 +405,23 @@ export const CreateConnection = (props: CreateConnectionProps) => {
405405
}, [enabledCapabilities, parametersByCapability]);
406406

407407
// Don't show name for simple connections
408-
const showNameInput = useMemo(
409-
() =>
408+
const showNameInput = useMemo(() => {
409+
const isMcpClientConnection = connectorId?.toLowerCase().includes('mcpclient');
410+
411+
if (isMcpClientConnection) {
412+
const connectionService = ConnectionService();
413+
const isConsumptionSku = connectionService.constructor.name === 'ConsumptionConnectionService';
414+
415+
if (isConsumptionSku) {
416+
return false;
417+
}
418+
}
419+
420+
return (
410421
!(isUsingOAuth && !isMultiAuth) &&
411-
(isMultiAuth || Object.keys(capabilityEnabledParameters ?? {}).length > 0 || legacyManagedIdentitySelected),
412-
[isUsingOAuth, isMultiAuth, capabilityEnabledParameters, legacyManagedIdentitySelected]
413-
);
422+
(isMultiAuth || Object.keys(capabilityEnabledParameters ?? {}).length > 0 || legacyManagedIdentitySelected)
423+
);
424+
}, [connectorId, isUsingOAuth, isMultiAuth, capabilityEnabledParameters, legacyManagedIdentitySelected]);
414425

415426
const validParams = useMemo(() => {
416427
if (showNameInput && !connectionDisplayName) {
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest';
2+
import { ConsumptionConnectionService } from '../connection';
3+
import type { Connector } from '../../../../utils/src';
4+
import type { ConnectionCreationInfo } from '../../connection';
5+
import { InitLoggerService } from '../../logger';
6+
7+
// Mock the LoggerService
8+
const mockLoggerService = {
9+
log: vi.fn(),
10+
startTrace: vi.fn().mockReturnValue('mock-trace-id'),
11+
endTrace: vi.fn(),
12+
logErrorWithFormatting: vi.fn(),
13+
};
14+
15+
describe('ConsumptionConnectionService', () => {
16+
const mockHttpClient = {
17+
get: vi.fn(),
18+
post: vi.fn(),
19+
put: vi.fn(),
20+
delete: vi.fn(),
21+
};
22+
23+
const mockOptions = {
24+
apiVersion: '2018-07-01-preview',
25+
baseUrl: 'https://management.azure.com',
26+
subscriptionId: 'test-sub',
27+
resourceGroup: 'test-rg',
28+
location: 'eastus',
29+
httpClient: mockHttpClient,
30+
apiHubServiceDetails: {
31+
apiVersion: '2018-07-01-preview',
32+
baseUrl: 'https://management.azure.com',
33+
subscriptionId: 'test-sub',
34+
resourceGroup: 'test-rg',
35+
location: 'eastus',
36+
httpClient: mockHttpClient,
37+
},
38+
};
39+
40+
let service: ConsumptionConnectionService;
41+
42+
beforeEach(() => {
43+
// Initialize the logger service before each test
44+
InitLoggerService([mockLoggerService]);
45+
46+
service = new ConsumptionConnectionService(mockOptions as any);
47+
vi.clearAllMocks();
48+
49+
// Re-setup logger mocks after clearAllMocks
50+
mockLoggerService.startTrace.mockReturnValue('mock-trace-id');
51+
});
52+
53+
describe('createBuiltInMcpConnection', () => {
54+
it('should create a built-in MCP connection with correct structure', async () => {
55+
const connector: Partial<Connector> = {
56+
id: 'connectionProviders/mcpclient',
57+
type: 'connectionProviders/mcpclient',
58+
name: 'mcpclient',
59+
properties: {
60+
displayName: 'MCP Client',
61+
iconUri: 'https://example.com/icon.png',
62+
brandColor: '#000000',
63+
capabilities: ['builtin'],
64+
description: 'MCP Client Connector',
65+
generalInformation: {
66+
displayName: 'MCP Client',
67+
iconUrl: 'https://example.com/icon.png',
68+
},
69+
},
70+
};
71+
72+
const connectionInfo: ConnectionCreationInfo = {
73+
displayName: 'test-mcp-connection',
74+
connectionParameters: {
75+
serverUrl: { value: 'https://mcp-server.example.com' },
76+
},
77+
connectionParametersSet: {
78+
name: 'ApiKey',
79+
values: {
80+
key: { value: 'test-api-key' },
81+
},
82+
},
83+
};
84+
85+
const result = await service.createConnection('test-connection-id', connector as Connector, connectionInfo);
86+
87+
expect(result).toBeDefined();
88+
expect(result.name).toBe('test-mcp-connection');
89+
expect(result.id).toContain('connectionProviders/mcpclient/connections/');
90+
expect((result.properties as any).parameterValues.mcpServerUrl).toBe('https://mcp-server.example.com');
91+
expect((result.properties as any).parameterValues.authenticationType).toBe('ApiKey');
92+
});
93+
94+
it('should throw error when serverUrl is missing', async () => {
95+
const connector: Partial<Connector> = {
96+
id: 'connectionProviders/mcpclient',
97+
type: 'connectionProviders/mcpclient',
98+
name: 'mcpclient',
99+
properties: {
100+
displayName: 'MCP Client',
101+
capabilities: ['builtin'],
102+
generalInformation: {
103+
displayName: 'MCP Client',
104+
},
105+
iconUri: '',
106+
},
107+
};
108+
109+
const connectionInfo: ConnectionCreationInfo = {
110+
displayName: 'test-mcp-connection',
111+
connectionParameters: {},
112+
};
113+
114+
await expect(service.createConnection('test-connection-id', connector as Connector, connectionInfo)).rejects.toThrow(
115+
'Server URL is required for MCP connection'
116+
);
117+
});
118+
119+
it('should use connectionId as fallback name when displayName is not provided', async () => {
120+
const connector: Partial<Connector> = {
121+
id: 'connectionProviders/mcpclient',
122+
type: 'connectionProviders/mcpclient',
123+
name: 'mcpclient',
124+
properties: {
125+
displayName: 'MCP Client',
126+
capabilities: ['builtin'],
127+
generalInformation: {
128+
displayName: 'MCP Client',
129+
},
130+
iconUri: '',
131+
},
132+
};
133+
134+
const connectionInfo: ConnectionCreationInfo = {
135+
connectionParameters: {
136+
serverUrl: { value: 'https://mcp-server.example.com' },
137+
},
138+
};
139+
140+
const result = await service.createConnection(
141+
'/subscriptions/sub/connections/my-connection-name',
142+
connector as Connector,
143+
connectionInfo
144+
);
145+
146+
expect(result.name).toBe('my-connection-name');
147+
});
148+
149+
it('should handle None authentication type', async () => {
150+
const connector: Partial<Connector> = {
151+
id: 'connectionProviders/mcpclient',
152+
type: 'connectionProviders/mcpclient',
153+
name: 'mcpclient',
154+
properties: {
155+
displayName: 'MCP Client',
156+
capabilities: ['builtin'],
157+
generalInformation: {
158+
displayName: 'MCP Client',
159+
},
160+
iconUri: '',
161+
},
162+
};
163+
164+
const connectionInfo: ConnectionCreationInfo = {
165+
displayName: 'test-mcp-connection',
166+
connectionParameters: {
167+
serverUrl: { value: 'https://mcp-server.example.com' },
168+
},
169+
connectionParametersSet: {
170+
name: 'None',
171+
values: {},
172+
},
173+
};
174+
175+
const result = await service.createConnection('test-connection-id', connector as Connector, connectionInfo);
176+
177+
expect((result.properties as any).parameterValues.authenticationType).toBe('None');
178+
});
179+
});
180+
181+
describe('extractParameterValue', () => {
182+
it('should extract value from wrapped object', () => {
183+
const result = (service as any).extractParameterValue({ value: 'test' });
184+
expect(result).toBe('test');
185+
});
186+
187+
it('should return direct value if not wrapped', () => {
188+
const result = (service as any).extractParameterValue('direct-value');
189+
expect(result).toBe('direct-value');
190+
});
191+
192+
it('should handle null value', () => {
193+
const result = (service as any).extractParameterValue(null);
194+
expect(result).toBe(null);
195+
});
196+
197+
it('should handle undefined value', () => {
198+
const result = (service as any).extractParameterValue(undefined);
199+
expect(result).toBe(undefined);
200+
});
201+
202+
it('should handle object without value property', () => {
203+
const result = (service as any).extractParameterValue({ other: 'prop' });
204+
expect(result).toEqual({ other: 'prop' });
205+
});
206+
});
207+
208+
describe('extractAuthParameters', () => {
209+
it('should extract authentication parameters correctly', () => {
210+
const result = (service as any).extractAuthParameters({
211+
name: 'ApiKey',
212+
values: {
213+
key: { value: 'my-api-key' },
214+
keyHeaderName: { value: 'X-API-Key' },
215+
},
216+
});
217+
218+
expect(result.authenticationType).toBe('ApiKey');
219+
expect(result.authParams.key).toBe('my-api-key');
220+
expect(result.authParams.keyHeaderName).toBe('X-API-Key');
221+
});
222+
223+
it('should return None for undefined connectionParametersSet', () => {
224+
const result = (service as any).extractAuthParameters(undefined);
225+
226+
expect(result.authenticationType).toBe('None');
227+
expect(result.authParams).toEqual({});
228+
});
229+
230+
it('should return None for null connectionParametersSet', () => {
231+
const result = (service as any).extractAuthParameters(null);
232+
233+
expect(result.authenticationType).toBe('None');
234+
expect(result.authParams).toEqual({});
235+
});
236+
237+
it('should handle BasicAuth parameters', () => {
238+
const result = (service as any).extractAuthParameters({
239+
name: 'BasicAuth',
240+
values: {
241+
username: { value: 'testuser' },
242+
password: { value: 'testpass' },
243+
},
244+
});
245+
246+
expect(result.authenticationType).toBe('BasicAuth');
247+
expect(result.authParams.username).toBe('testuser');
248+
expect(result.authParams.password).toBe('testpass');
249+
});
250+
251+
it('should handle OAuth2 parameters', () => {
252+
const result = (service as any).extractAuthParameters({
253+
name: 'OAuth2',
254+
values: {
255+
clientId: { value: 'client-123' },
256+
secret: { value: 'secret-456' },
257+
tenant: { value: 'tenant-789' },
258+
authority: { value: 'https://login.microsoftonline.com' },
259+
audience: { value: 'api://my-app' },
260+
},
261+
});
262+
263+
expect(result.authenticationType).toBe('OAuth2');
264+
expect(result.authParams.clientId).toBe('client-123');
265+
expect(result.authParams.secret).toBe('secret-456');
266+
expect(result.authParams.tenant).toBe('tenant-789');
267+
expect(result.authParams.authority).toBe('https://login.microsoftonline.com');
268+
expect(result.authParams.audience).toBe('api://my-app');
269+
});
270+
271+
it('should only extract known auth keys', () => {
272+
const result = (service as any).extractAuthParameters({
273+
name: 'Custom',
274+
values: {
275+
key: { value: 'valid-key' },
276+
unknownParam: { value: 'should-be-ignored' },
277+
anotherUnknown: { value: 'also-ignored' },
278+
},
279+
});
280+
281+
expect(result.authParams.key).toBe('valid-key');
282+
expect(result.authParams.unknownParam).toBeUndefined();
283+
expect(result.authParams.anotherUnknown).toBeUndefined();
284+
});
285+
});
286+
287+
describe('getConnector', () => {
288+
it('should return mcpclient connector for mcpclient connectorId', async () => {
289+
const result = await service.getConnector('connectionProviders/mcpclient');
290+
291+
expect(result).toBeDefined();
292+
expect(result.id).toContain('mcpclient');
293+
});
294+
295+
it('should return agent connector for agent connectorId', async () => {
296+
const result = await service.getConnector('connectionProviders/agent');
297+
298+
expect(result).toBeDefined();
299+
});
300+
});
301+
});

0 commit comments

Comments
 (0)