diff --git a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesignerConsumption.tsx b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesignerConsumption.tsx index 111fe8944f2..7636afd0292 100644 --- a/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesignerConsumption.tsx +++ b/apps/Standalone/src/designer/app/AzureLogicAppsDesigner/laDesignerConsumption.tsx @@ -309,6 +309,7 @@ const DesignerEditorConsumption = () => { ...getSKUDefaultHostOptions(Constants.SKU.CONSUMPTION), }, showPerformanceDebug, + mcpClientToolEnabled: true, }} > {definition ? ( diff --git a/libs/designer-v2/src/lib/ui/panel/connectionsPanel/createConnection/createConnection.tsx b/libs/designer-v2/src/lib/ui/panel/connectionsPanel/createConnection/createConnection.tsx index 31da5ecc740..764f6a4ebb7 100644 --- a/libs/designer-v2/src/lib/ui/panel/connectionsPanel/createConnection/createConnection.tsx +++ b/libs/designer-v2/src/lib/ui/panel/connectionsPanel/createConnection/createConnection.tsx @@ -21,6 +21,7 @@ import { import { ConnectionParameterEditorService, ConnectionService, + ConsumptionConnectionService, Capabilities, ConnectionParameterTypes, SERVICE_PRINCIPLE_CONSTANTS, @@ -405,12 +406,23 @@ export const CreateConnection = (props: CreateConnectionProps) => { }, [enabledCapabilities, parametersByCapability]); // Don't show name for simple connections - const showNameInput = useMemo( - () => + const showNameInput = useMemo(() => { + const isMcpClientConnection = connectorId?.toLowerCase().includes('mcpclient'); + + if (isMcpClientConnection) { + const connectionService = ConnectionService(); + const isConsumptionSku = connectionService instanceof ConsumptionConnectionService; + + if (isConsumptionSku) { + return false; + } + } + + return ( !(isUsingOAuth && !isMultiAuth) && - (isMultiAuth || Object.keys(capabilityEnabledParameters ?? {}).length > 0 || legacyManagedIdentitySelected), - [isUsingOAuth, isMultiAuth, capabilityEnabledParameters, legacyManagedIdentitySelected] - ); + (isMultiAuth || Object.keys(capabilityEnabledParameters ?? {}).length > 0 || legacyManagedIdentitySelected) + ); + }, [connectorId, isUsingOAuth, isMultiAuth, capabilityEnabledParameters, legacyManagedIdentitySelected]); const validParams = useMemo(() => { if (showNameInput && !connectionDisplayName) { diff --git a/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/createConnection.tsx b/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/createConnection.tsx index a5f26e4dab7..f26fc97b67c 100644 --- a/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/createConnection.tsx +++ b/libs/designer/src/lib/ui/panel/connectionsPanel/createConnection/createConnection.tsx @@ -21,6 +21,7 @@ import { import { ConnectionParameterEditorService, ConnectionService, + ConsumptionConnectionService, Capabilities, ConnectionParameterTypes, SERVICE_PRINCIPLE_CONSTANTS, @@ -407,12 +408,23 @@ export const CreateConnection = (props: CreateConnectionProps) => { }, [enabledCapabilities, parametersByCapability]); // Don't show name for simple connections - const showNameInput = useMemo( - () => + const showNameInput = useMemo(() => { + const isMcpClientConnection = connectorId?.toLowerCase().includes('mcpclient'); + + if (isMcpClientConnection) { + const connectionService = ConnectionService(); + const isConsumptionSku = connectionService instanceof ConsumptionConnectionService; + + if (isConsumptionSku) { + return false; + } + } + + return ( !(isUsingOAuth && !isMultiAuth) && - (isMultiAuth || Object.keys(capabilityEnabledParameters ?? {}).length > 0 || legacyManagedIdentitySelected), - [isUsingOAuth, isMultiAuth, capabilityEnabledParameters, legacyManagedIdentitySelected] - ); + (isMultiAuth || Object.keys(capabilityEnabledParameters ?? {}).length > 0 || legacyManagedIdentitySelected) + ); + }, [connectorId, isUsingOAuth, isMultiAuth, capabilityEnabledParameters, legacyManagedIdentitySelected]); const validParams = useMemo(() => { if (showNameInput && !connectionDisplayName) { diff --git a/libs/logic-apps-shared/src/designer-client-services/lib/consumption/__tests__/connection.spec.ts b/libs/logic-apps-shared/src/designer-client-services/lib/consumption/__tests__/connection.spec.ts new file mode 100644 index 00000000000..b0515529c69 --- /dev/null +++ b/libs/logic-apps-shared/src/designer-client-services/lib/consumption/__tests__/connection.spec.ts @@ -0,0 +1,301 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { ConsumptionConnectionService } from '../connection'; +import type { Connector } from '../../../../utils/src'; +import type { ConnectionCreationInfo } from '../../connection'; +import { InitLoggerService } from '../../logger'; + +// Mock the LoggerService +const mockLoggerService = { + log: vi.fn(), + startTrace: vi.fn().mockReturnValue('mock-trace-id'), + endTrace: vi.fn(), + logErrorWithFormatting: vi.fn(), +}; + +describe('ConsumptionConnectionService', () => { + const mockHttpClient = { + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn(), + }; + + const mockOptions = { + apiVersion: '2018-07-01-preview', + baseUrl: 'https://management.azure.com', + subscriptionId: 'test-sub', + resourceGroup: 'test-rg', + location: 'eastus', + httpClient: mockHttpClient, + apiHubServiceDetails: { + apiVersion: '2018-07-01-preview', + baseUrl: 'https://management.azure.com', + subscriptionId: 'test-sub', + resourceGroup: 'test-rg', + location: 'eastus', + httpClient: mockHttpClient, + }, + }; + + let service: ConsumptionConnectionService; + + beforeEach(() => { + // Initialize the logger service before each test + InitLoggerService([mockLoggerService]); + + service = new ConsumptionConnectionService(mockOptions as any); + vi.clearAllMocks(); + + // Re-setup logger mocks after clearAllMocks + mockLoggerService.startTrace.mockReturnValue('mock-trace-id'); + }); + + describe('createBuiltInMcpConnection', () => { + it('should create a built-in MCP connection with correct structure', async () => { + const connector: Partial = { + id: 'connectionProviders/mcpclient', + type: 'connectionProviders/mcpclient', + name: 'mcpclient', + properties: { + displayName: 'MCP Client', + iconUri: 'https://example.com/icon.png', + brandColor: '#000000', + capabilities: ['builtin'], + description: 'MCP Client Connector', + generalInformation: { + displayName: 'MCP Client', + iconUrl: 'https://example.com/icon.png', + }, + }, + }; + + const connectionInfo: ConnectionCreationInfo = { + displayName: 'test-mcp-connection', + connectionParameters: { + serverUrl: { value: 'https://mcp-server.example.com' }, + }, + connectionParametersSet: { + name: 'ApiKey', + values: { + key: { value: 'test-api-key' }, + }, + }, + }; + + const result = await service.createConnection('test-connection-id', connector as Connector, connectionInfo); + + expect(result).toBeDefined(); + expect(result.name).toBe('test-mcp-connection'); + expect(result.id).toContain('connectionProviders/mcpclient/connections/'); + expect((result.properties as any).parameterValues.mcpServerUrl).toBe('https://mcp-server.example.com'); + expect((result.properties as any).parameterValues.authenticationType).toBe('ApiKey'); + }); + + it('should throw error when serverUrl is missing', async () => { + const connector: Partial = { + id: 'connectionProviders/mcpclient', + type: 'connectionProviders/mcpclient', + name: 'mcpclient', + properties: { + displayName: 'MCP Client', + capabilities: ['builtin'], + generalInformation: { + displayName: 'MCP Client', + }, + iconUri: '', + }, + }; + + const connectionInfo: ConnectionCreationInfo = { + displayName: 'test-mcp-connection', + connectionParameters: {}, + }; + + await expect(service.createConnection('test-connection-id', connector as Connector, connectionInfo)).rejects.toThrow( + 'Server URL is required for MCP connection' + ); + }); + + it('should use connectionId as fallback name when displayName is not provided', async () => { + const connector: Partial = { + id: 'connectionProviders/mcpclient', + type: 'connectionProviders/mcpclient', + name: 'mcpclient', + properties: { + displayName: 'MCP Client', + capabilities: ['builtin'], + generalInformation: { + displayName: 'MCP Client', + }, + iconUri: '', + }, + }; + + const connectionInfo: ConnectionCreationInfo = { + connectionParameters: { + serverUrl: { value: 'https://mcp-server.example.com' }, + }, + }; + + const result = await service.createConnection( + '/subscriptions/sub/connections/my-connection-name', + connector as Connector, + connectionInfo + ); + + expect(result.name).toBe('my-connection-name'); + }); + + it('should handle None authentication type', async () => { + const connector: Partial = { + id: 'connectionProviders/mcpclient', + type: 'connectionProviders/mcpclient', + name: 'mcpclient', + properties: { + displayName: 'MCP Client', + capabilities: ['builtin'], + generalInformation: { + displayName: 'MCP Client', + }, + iconUri: '', + }, + }; + + const connectionInfo: ConnectionCreationInfo = { + displayName: 'test-mcp-connection', + connectionParameters: { + serverUrl: { value: 'https://mcp-server.example.com' }, + }, + connectionParametersSet: { + name: 'None', + values: {}, + }, + }; + + const result = await service.createConnection('test-connection-id', connector as Connector, connectionInfo); + + expect((result.properties as any).parameterValues.authenticationType).toBe('None'); + }); + }); + + describe('extractParameterValue', () => { + it('should extract value from wrapped object', () => { + const result = (service as any).extractParameterValue({ value: 'test' }); + expect(result).toBe('test'); + }); + + it('should return direct value if not wrapped', () => { + const result = (service as any).extractParameterValue('direct-value'); + expect(result).toBe('direct-value'); + }); + + it('should handle null value', () => { + const result = (service as any).extractParameterValue(null); + expect(result).toBe(null); + }); + + it('should handle undefined value', () => { + const result = (service as any).extractParameterValue(undefined); + expect(result).toBe(undefined); + }); + + it('should handle object without value property', () => { + const result = (service as any).extractParameterValue({ other: 'prop' }); + expect(result).toEqual({ other: 'prop' }); + }); + }); + + describe('extractAuthParameters', () => { + it('should extract authentication parameters correctly', () => { + const result = (service as any).extractAuthParameters({ + name: 'ApiKey', + values: { + key: { value: 'my-api-key' }, + keyHeaderName: { value: 'X-API-Key' }, + }, + }); + + expect(result.authenticationType).toBe('ApiKey'); + expect(result.authParams.key).toBe('my-api-key'); + expect(result.authParams.keyHeaderName).toBe('X-API-Key'); + }); + + it('should return None for undefined connectionParametersSet', () => { + const result = (service as any).extractAuthParameters(undefined); + + expect(result.authenticationType).toBe('None'); + expect(result.authParams).toEqual({}); + }); + + it('should return None for null connectionParametersSet', () => { + const result = (service as any).extractAuthParameters(null); + + expect(result.authenticationType).toBe('None'); + expect(result.authParams).toEqual({}); + }); + + it('should handle BasicAuth parameters', () => { + const result = (service as any).extractAuthParameters({ + name: 'BasicAuth', + values: { + username: { value: 'testuser' }, + password: { value: 'testpass' }, + }, + }); + + expect(result.authenticationType).toBe('BasicAuth'); + expect(result.authParams.username).toBe('testuser'); + expect(result.authParams.password).toBe('testpass'); + }); + + it('should handle OAuth2 parameters', () => { + const result = (service as any).extractAuthParameters({ + name: 'OAuth2', + values: { + clientId: { value: 'client-123' }, + secret: { value: 'secret-456' }, + tenant: { value: 'tenant-789' }, + authority: { value: 'https://login.microsoftonline.com' }, + audience: { value: 'api://my-app' }, + }, + }); + + expect(result.authenticationType).toBe('OAuth2'); + expect(result.authParams.clientId).toBe('client-123'); + expect(result.authParams.secret).toBe('secret-456'); + expect(result.authParams.tenant).toBe('tenant-789'); + expect(result.authParams.authority).toBe('https://login.microsoftonline.com'); + expect(result.authParams.audience).toBe('api://my-app'); + }); + + it('should only extract known auth keys', () => { + const result = (service as any).extractAuthParameters({ + name: 'Custom', + values: { + key: { value: 'valid-key' }, + unknownParam: { value: 'should-be-ignored' }, + anotherUnknown: { value: 'also-ignored' }, + }, + }); + + expect(result.authParams.key).toBe('valid-key'); + expect(result.authParams.unknownParam).toBeUndefined(); + expect(result.authParams.anotherUnknown).toBeUndefined(); + }); + }); + + describe('getConnector', () => { + it('should return mcpclient connector for mcpclient connectorId', async () => { + const result = await service.getConnector('connectionProviders/mcpclient'); + + expect(result).toBeDefined(); + expect(result.id).toContain('mcpclient'); + }); + + it('should return agent connector for agent connectorId', async () => { + const result = await service.getConnector('connectionProviders/agent'); + + expect(result).toBeDefined(); + }); + }); +}); diff --git a/libs/logic-apps-shared/src/designer-client-services/lib/consumption/__tests__/connector.spec.ts b/libs/logic-apps-shared/src/designer-client-services/lib/consumption/__tests__/connector.spec.ts new file mode 100644 index 00000000000..55396f333c5 --- /dev/null +++ b/libs/logic-apps-shared/src/designer-client-services/lib/consumption/__tests__/connector.spec.ts @@ -0,0 +1,398 @@ +import { describe, vi, beforeEach, it, expect } from 'vitest'; +import { ConsumptionConnectorService } from '../connector'; +import type { IHttpClient } from '../../httpClient'; +import { InitConnectionService } from '../../connection'; +import type { Connection } from '../../../../utils/src'; + +describe('ConsumptionConnectorService', () => { + let mockHttpClient: IHttpClient; + let connectorService: ConsumptionConnectorService; + + const baseUrl = 'https://management.azure.com'; + const workflowReferenceId = '/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Logic/workflows/test-workflow'; + const apiVersion = '2018-07-01-preview'; + + const createMockOptions = () => ({ + apiVersion, + baseUrl, + httpClient: {} as IHttpClient, + workflowReferenceId, + clientSupportedOperations: [] as { connectorId: string; operationId: string }[], + schemaClient: {}, + valuesClient: {}, + }); + + beforeEach(() => { + mockHttpClient = { + dispose: vi.fn(), + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + patch: vi.fn(), + delete: vi.fn(), + }; + + const options = createMockOptions(); + options.httpClient = mockHttpClient; + connectorService = new ConsumptionConnectorService(options); + }); + + describe('constructor', () => { + it('should throw when workflowReferenceId is not provided', () => { + expect( + () => + new ConsumptionConnectorService({ + ...createMockOptions(), + workflowReferenceId: '', + }) + ).toThrow('workflowReferenceId required'); + }); + }); + + describe('getListDynamicValues', () => { + const mcpDynamicState = { + operationId: 'listMcpTools', + apiType: 'mcp', + }; + + const nonMcpDynamicState = { + operationId: 'someOperation', + parameters: {}, + }; + + const mockMcpToolsResponse = { + response: { + statusCode: 'OK', + body: [ + { name: 'tool1', description: 'First tool' }, + { name: 'tool2', description: 'Second tool' }, + ], + headers: {}, + }, + }; + + describe('MCP built-in connections', () => { + const builtInConnectionId = '/connectionProviders/mcpclient/connections/test-mcp'; + + beforeEach(() => { + InitConnectionService({ + getConnection: vi.fn().mockResolvedValue({ + id: builtInConnectionId, + name: 'test-mcp', + properties: { + displayName: 'Test MCP Server', + parameterValues: { + mcpServerUrl: 'https://mcp.example.com', + authenticationType: 'None', + }, + }, + } as unknown as Connection), + } as any); + }); + + it('should call listMcpTools endpoint with correct URL', async () => { + vi.mocked(mockHttpClient.post).mockResolvedValue(mockMcpToolsResponse); + + await connectorService.getListDynamicValues( + builtInConnectionId, + '/connectionProviders/mcpclient', + 'nativemcpclient', + {}, + mcpDynamicState + ); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + expect.objectContaining({ + uri: `${baseUrl}${workflowReferenceId}/listMcpTools`, + }) + ); + }); + + it('should send native connection shape with mcpServerUrl', async () => { + vi.mocked(mockHttpClient.post).mockResolvedValue(mockMcpToolsResponse); + + await connectorService.getListDynamicValues( + builtInConnectionId, + '/connectionProviders/mcpclient', + 'nativemcpclient', + {}, + mcpDynamicState + ); + + const postCallArgs = vi.mocked(mockHttpClient.post).mock.calls[0][0]; + const content = postCallArgs.content as any; + + expect(content.connection).toBeDefined(); + expect(content.connection.mcpServerUrl).toBe('https://mcp.example.com'); + expect(content.connection.displayName).toBe('Test MCP Server'); + }); + + it('should include authentication for ApiKey auth type', async () => { + InitConnectionService({ + getConnection: vi.fn().mockResolvedValue({ + id: builtInConnectionId, + name: 'test-mcp', + properties: { + displayName: 'Test MCP Server', + parameterValues: { + mcpServerUrl: 'https://mcp.example.com', + authenticationType: 'ApiKey', + key: 'test-api-key', + keyHeaderName: 'X-Api-Key', + }, + }, + } as unknown as Connection), + } as any); + + vi.mocked(mockHttpClient.post).mockResolvedValue(mockMcpToolsResponse); + + await connectorService.getListDynamicValues( + builtInConnectionId, + '/connectionProviders/mcpclient', + 'nativemcpclient', + {}, + mcpDynamicState + ); + + const postCallArgs = vi.mocked(mockHttpClient.post).mock.calls[0][0]; + const content = postCallArgs.content as any; + + expect(content.connection.authentication).toEqual({ + type: 'ApiKey', + value: 'test-api-key', + name: 'X-Api-Key', + in: 'header', + }); + }); + + it('should not include authentication when auth type is None', async () => { + vi.mocked(mockHttpClient.post).mockResolvedValue(mockMcpToolsResponse); + + await connectorService.getListDynamicValues( + builtInConnectionId, + '/connectionProviders/mcpclient', + 'nativemcpclient', + {}, + mcpDynamicState + ); + + const postCallArgs = vi.mocked(mockHttpClient.post).mock.calls[0][0]; + const content = postCallArgs.content as any; + + expect(content.connection.authentication).toBeUndefined(); + }); + + it('should map tools response to ListDynamicValue array', async () => { + vi.mocked(mockHttpClient.post).mockResolvedValue(mockMcpToolsResponse); + + const result = await connectorService.getListDynamicValues( + builtInConnectionId, + '/connectionProviders/mcpclient', + 'nativemcpclient', + {}, + mcpDynamicState + ); + + expect(result).toEqual([ + { value: 'tool1', displayName: 'tool1', description: 'First tool' }, + { value: 'tool2', displayName: 'tool2', description: 'Second tool' }, + ]); + }); + + it('should return empty array when tools response is null', async () => { + vi.mocked(mockHttpClient.post).mockResolvedValue({ + response: { statusCode: 'OK', body: null, headers: {} }, + }); + + const result = await connectorService.getListDynamicValues( + builtInConnectionId, + '/connectionProviders/mcpclient', + 'nativemcpclient', + {}, + mcpDynamicState + ); + + expect(result).toEqual([]); + }); + + it('should pass mcpServerPath from operationPath', async () => { + vi.mocked(mockHttpClient.post).mockResolvedValue(mockMcpToolsResponse); + + await connectorService.getListDynamicValues( + builtInConnectionId, + '/connectionProviders/mcpclient', + 'nativemcpclient', + {}, + mcpDynamicState, + false, + '/custom/mcp/path' + ); + + const postCallArgs = vi.mocked(mockHttpClient.post).mock.calls[0][0]; + const content = postCallArgs.content as any; + + expect(content.mcpServerPath).toBe('/custom/mcp/path'); + }); + }); + + describe('MCP managed connections', () => { + const managedConnectionId = '/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Web/connections/mcp-managed'; + + beforeEach(() => { + InitConnectionService({ + getConnection: vi.fn().mockResolvedValue({ + id: managedConnectionId, + name: 'mcp-managed', + properties: { + displayName: 'Managed MCP', + }, + } as unknown as Connection), + } as any); + }); + + it('should send managedConnection shape for non-builtin connections', async () => { + vi.mocked(mockHttpClient.post).mockResolvedValue(mockMcpToolsResponse); + + await connectorService.getListDynamicValues( + managedConnectionId, + '/connectionProviders/mcpclient', + 'nativemcpclient', + {}, + mcpDynamicState, + false, + '/mcp/path' + ); + + const postCallArgs = vi.mocked(mockHttpClient.post).mock.calls[0][0]; + const content = postCallArgs.content as any; + + expect(content.managedConnection).toEqual({ + connection: { id: managedConnectionId }, + }); + expect(content.mcpServerPath).toBe('/mcp/path'); + expect(content.connection).toBeUndefined(); + }); + }); + + describe('MCP with no connectionId', () => { + it('should send only mcpServerPath when connectionId is undefined', async () => { + vi.mocked(mockHttpClient.post).mockResolvedValue(mockMcpToolsResponse); + + await connectorService.getListDynamicValues( + undefined, + '/connectionProviders/mcpclient', + 'nativemcpclient', + {}, + mcpDynamicState, + false, + '/mcp/path' + ); + + const postCallArgs = vi.mocked(mockHttpClient.post).mock.calls[0][0]; + const content = postCallArgs.content as any; + + expect(content).toEqual({ mcpServerPath: '/mcp/path' }); + }); + }); + + describe('non-MCP dynamic list', () => { + it('should call dynamicList endpoint for non-MCP connections', async () => { + const connectionId = '/some/connection/id'; + + vi.mocked(mockHttpClient.post).mockResolvedValue({ + response: { + statusCode: 'OK', + body: { value: [{ value: 'option1', displayName: 'Option 1' }] }, + headers: {}, + }, + }); + + await connectorService.getListDynamicValues(connectionId, 'someConnector', 'someOperation', {}, nonMcpDynamicState); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + expect.objectContaining({ + uri: `${connectionId}/dynamicList`, + }) + ); + }); + }); + }); + + describe('_buildMcpAuthentication', () => { + // Access private method via any cast for testing + const buildAuth = (props: Record) => { + return (connectorService as any)._buildMcpAuthentication(props); + }; + + it('should return undefined for None auth type', () => { + expect(buildAuth({ authenticationType: 'None' })).toBeUndefined(); + }); + + it('should return undefined when no auth type', () => { + expect(buildAuth({})).toBeUndefined(); + }); + + it('should build ApiKey authentication', () => { + const result = buildAuth({ + authenticationType: 'ApiKey', + key: 'my-key', + keyHeaderName: 'Authorization', + }); + + expect(result).toEqual({ + type: 'ApiKey', + value: 'my-key', + name: 'Authorization', + in: 'header', + }); + }); + + it('should build Basic authentication', () => { + const result = buildAuth({ + authenticationType: 'Basic', + username: 'user', + password: 'pass', + }); + + expect(result).toEqual({ + type: 'Basic', + username: 'user', + password: 'pass', + }); + }); + + it('should build ActiveDirectoryOAuth authentication', () => { + const result = buildAuth({ + authenticationType: 'ActiveDirectoryOAuth', + tenant: 'my-tenant', + clientId: 'my-client', + secret: 'my-secret', + authority: 'https://login.microsoftonline.com', + audience: 'https://api.example.com', + }); + + expect(result).toEqual({ + type: 'ActiveDirectoryOAuth', + tenant: 'my-tenant', + clientId: 'my-client', + secret: 'my-secret', + authority: 'https://login.microsoftonline.com', + audience: 'https://api.example.com', + }); + }); + + it('should build ClientCertificate authentication', () => { + const result = buildAuth({ + authenticationType: 'ClientCertificate', + pfx: 'cert-data', + password: 'cert-pass', + }); + + expect(result).toEqual({ + type: 'ClientCertificate', + pfx: 'cert-data', + password: 'cert-pass', + }); + }); + }); +}); diff --git a/libs/logic-apps-shared/src/designer-client-services/lib/consumption/__tests__/operationmanifest.spec.ts b/libs/logic-apps-shared/src/designer-client-services/lib/consumption/__tests__/operationmanifest.spec.ts index 4e25a2c1c30..f2920c6652e 100644 --- a/libs/logic-apps-shared/src/designer-client-services/lib/consumption/__tests__/operationmanifest.spec.ts +++ b/libs/logic-apps-shared/src/designer-client-services/lib/consumption/__tests__/operationmanifest.spec.ts @@ -29,6 +29,18 @@ describe('ConsumptionOperationManifestService', () => { }); describe('getOperationInfo', () => { + test('should return correct operation info for mcpclienttool type', async () => { + const definition = { + type: 'McpClientTool', + inputs: {}, + }; + + const result = await operationManifestService.getOperationInfo(definition, false); + + expect(result.connectorId).toBe('connectionProviders/mcpclient'); + expect(result.operationId).toBe('nativemcpclient'); + }); + test('should return correct operation info for workflow type', async () => { const definition = { type: 'workflow', @@ -91,6 +103,11 @@ describe('ConsumptionOperationManifestService', () => { }); describe('isSupported', () => { + test('should return true for mcpclienttool builtin operation type', () => { + const result = operationManifestService.isSupported('mcpclienttool', 'builtin'); + expect(result).toBe(true); + }); + test('should return true for nestedagent operation type', () => { const result = operationManifestService.isSupported('nestedagent'); expect(result).toBe(true); @@ -108,6 +125,14 @@ describe('ConsumptionOperationManifestService', () => { }); describe('getOperationManifest', () => { + test('should return MCP manifest for nativemcpclient operation', async () => { + const result = await operationManifestService.getOperationManifest('connectionProviders/mcpclient', 'nativemcpclient'); + + expect(result).toBeDefined(); + expect(result.properties).toBeDefined(); + expect(result.properties.description).toBe('Uses an MCP server'); + }); + test('should return manifest for invokenestedagent operation', async () => { const result = await operationManifestService.getOperationManifest('/connectionProviders/workflow', 'invokenestedagent'); diff --git a/libs/logic-apps-shared/src/designer-client-services/lib/consumption/connection.ts b/libs/logic-apps-shared/src/designer-client-services/lib/consumption/connection.ts index b89ac826eb8..527a5e624f9 100644 --- a/libs/logic-apps-shared/src/designer-client-services/lib/consumption/connection.ts +++ b/libs/logic-apps-shared/src/designer-client-services/lib/consumption/connection.ts @@ -1,5 +1,5 @@ import type { QueryClient } from '@tanstack/react-query'; -import type { Connector, Connection } from '../../../utils/src'; +import type { Connector, Connection, ConnectionStatus } from '../../../utils/src'; import type { BaseConnectionServiceOptions } from '../base'; import { BaseConnectionService } from '../base'; import type { ConnectionCreationInfo, ConnectionParametersMetadata, CreateConnectionResult } from '../connection'; @@ -8,6 +8,7 @@ import { LogEntryLevel, Status } from '../logging/logEntry'; import type { IOAuthPopup } from '../oAuth'; import { OAuthService } from '../oAuth'; import agentLoopConnector from './manifests/agentLoopConnector'; +import mcpclientconnector from './manifests/mcpclientconnector'; export interface ConsumptionConnectionServiceOptions extends BaseConnectionServiceOptions { getCachedConnector?: (connectorId: string) => Promise; @@ -19,6 +20,49 @@ export class ConsumptionConnectionService extends BaseConnectionService { this._vVersion = 'V1'; } + private extractParameterValue(val: unknown): unknown { + if (typeof val === 'object' && val !== null && 'value' in val) { + return (val as { value: unknown }).value; + } + return val; + } + + private extractAuthParameters(connectionParametersSet: ConnectionCreationInfo['connectionParametersSet']): { + authenticationType: string; + authParams: Record; + } { + let authenticationType = 'None'; + const authParams: Record = {}; + + if (connectionParametersSet?.name && connectionParametersSet.name !== 'None') { + authenticationType = connectionParametersSet.name; + } + + if (connectionParametersSet?.values) { + const values = connectionParametersSet.values; + const authKeys = [ + 'username', + 'password', + 'key', + 'keyHeaderName', + 'value', + 'clientId', + 'secret', + 'tenant', + 'authority', + 'audience', + 'pfx', + ]; + for (const key of authKeys) { + if (values[key] !== undefined) { + authParams[key] = this.extractParameterValue(values[key]); + } + } + } + + return { authenticationType, authParams }; + } + async getConnector(connectorId: string, getCached = false): Promise { let connector: Connector | undefined; if (getCached && this._options.getCachedConnector) { @@ -28,6 +72,9 @@ export class ConsumptionConnectionService extends BaseConnectionService { if (connectorIdKeyword === 'agent') { return agentLoopConnector; } + if (connectorIdKeyword === 'mcpclient') { + return mcpclientconnector; + } return connector ?? this._getAzureConnector(connectorId); } @@ -49,6 +96,22 @@ export class ConsumptionConnectionService extends BaseConnectionService { _parametersMetadata?: ConnectionParametersMetadata, shouldTestConnection = true ): Promise { + const isBuiltInMcpConnection = + (connector.id?.toLowerCase().includes('mcpclient') || connector.type?.toLowerCase() === 'mcpclient') && + connector.properties?.capabilities?.includes('builtin'); + + if (isBuiltInMcpConnection) { + return this.createBuiltInMcpConnection(connectionId, connector, connectionInfo); + } + + const isManagedMcpConnection = + (connector.id?.toLowerCase().includes('mcpclient') || connector.type?.toLowerCase() === 'mcpclient') && + !connector.properties?.capabilities?.includes('builtin'); + + if (isManagedMcpConnection) { + return this.createManagedMcpConnection(connectionId, connector, connectionInfo); + } + const connectionName = connectionId.split('/').at(-1) as string; const logId = LoggerService().startTrace({ action: 'createConnection', @@ -83,6 +146,130 @@ export class ConsumptionConnectionService extends BaseConnectionService { } } + private createBuiltInMcpConnection(connectionId: string, connector: Connector, connectionInfo: ConnectionCreationInfo): Connection { + const logId = LoggerService().startTrace({ + action: 'createBuiltInMcpConnection', + name: 'Creating Built-in MCP Connection', + source: 'connection.ts', + }); + + try { + const connectionName = connectionInfo.displayName || connectionId.split('/').at(-1) || `mcp-${Date.now()}`; + + const connectionParameters = connectionInfo.connectionParameters ?? {}; + + let serverUrl = ''; + if (connectionParameters['serverUrl']) { + serverUrl = this.extractParameterValue(connectionParameters['serverUrl']) as string; + } + + if (!serverUrl) { + throw new Error('Server URL is required for MCP connection'); + } + + const { authenticationType, authParams } = this.extractAuthParameters(connectionInfo.connectionParametersSet); + + const connection = { + id: `/connectionProviders/mcpclient/connections/${connectionName}`, + name: connectionName, + type: 'connections', + location: '', + properties: { + displayName: connectionName, + overallStatus: 'Connected', + statuses: [{ status: 'Connected' }] as ConnectionStatus[], + api: { + id: connector.id, + name: 'mcpclient', + displayName: connector.properties?.displayName || 'MCP Client', + iconUri: connector.properties?.iconUri ?? '', + brandColor: connector.properties?.brandColor ?? '#000000', + description: connector.properties?.description ?? '', + category: 'MCP', + type: 'mcpclient', + }, + createdTime: new Date().toISOString(), + parameterValues: { + mcpServerUrl: serverUrl, + authenticationType, + ...authParams, + }, + }, + }; + + LoggerService().endTrace(logId, { status: Status.Success }); + const result = connection as unknown as Connection; + this._connections[result.id] = result; + return result; + } catch (error) { + const errorMessage = `Failed to create built-in MCP connection: ${this.tryParseErrorMessage(error)}`; + LoggerService().log({ + level: LogEntryLevel.Error, + area: 'createBuiltInMcpConnection', + message: errorMessage, + error: error instanceof Error ? error : undefined, + traceId: logId, + }); + LoggerService().endTrace(logId, { status: Status.Failure }); + throw new Error(errorMessage); + } + } + + private async createManagedMcpConnection( + connectionId: string, + connector: Connector, + connectionInfo: ConnectionCreationInfo + ): Promise { + const connectionName = connectionInfo.displayName || connectionId.split('/').at(-1) || `mcp-${Date.now()}`; + + const parameterValues: Record = {}; + const connectionParameters = connectionInfo.connectionParameters ?? {}; + + if (connectionParameters['serverUrl']) { + parameterValues['serverUrl'] = this.extractParameterValue(connectionParameters['serverUrl']); + } + + const { authenticationType, authParams } = this.extractAuthParameters(connectionInfo.connectionParametersSet); + if (authenticationType !== 'None') { + parameterValues['authentication'] = { + type: authenticationType, + ...authParams, + }; + } + + const mcpConnectionInfo: ConnectionCreationInfo = { + displayName: connectionName, + connectionParameters: parameterValues, + }; + + const logId = LoggerService().startTrace({ + action: 'createManagedMcpConnection', + name: 'Creating Managed MCP Connection', + source: 'connection.ts', + }); + + try { + const connection = await this.createConnectionInApiHub(connectionName, connector.id, mcpConnectionInfo); + + LoggerService().endTrace(logId, { + status: Status.Success, + data: { connectorId: connector.id }, + }); + return connection; + } catch (error) { + const errorMessage = `Failed to create managed MCP connection: ${this.tryParseErrorMessage(error)}`; + LoggerService().log({ + level: LogEntryLevel.Error, + area: 'createManagedMcpConnection', + message: errorMessage, + error: error instanceof Error ? error : undefined, + traceId: logId, + }); + LoggerService().endTrace(logId, { status: Status.Failure }); + return Promise.reject(errorMessage); + } + } + async createAndAuthorizeOAuthConnection( connectionId: string, connectorId: string, diff --git a/libs/logic-apps-shared/src/designer-client-services/lib/consumption/connector.ts b/libs/logic-apps-shared/src/designer-client-services/lib/consumption/connector.ts index bf58bf04382..0852fd42b59 100644 --- a/libs/logic-apps-shared/src/designer-client-services/lib/consumption/connector.ts +++ b/libs/logic-apps-shared/src/designer-client-services/lib/consumption/connector.ts @@ -2,6 +2,7 @@ import type { OpenAPIV2 } from '../../../utils/src'; import { ArgumentException, UnsupportedException, optional, equals, getResourceName } from '../../../utils/src'; import type { BaseConnectorServiceOptions } from '../base'; import { BaseConnectorService } from '../base'; +import { ConnectionService } from '../connection'; import type { ListDynamicValue, ManagedIdentityRequestProperties, TreeDynamicExtension, TreeDynamicValue } from '../connector'; import { pathCombine, unwrapPaginatedResponse } from '../helpers'; import { LoggerService } from '../logger'; @@ -55,7 +56,8 @@ export class ConsumptionConnectorService extends BaseConnectorService { isManagedIdentityConnection?: boolean ): Promise { const { apiVersion, httpClient } = this.options; - const { operationId: dynamicOperation } = dynamicState; + const { operationId: dynamicOperation, apiType } = dynamicState; + const isMcpConnection = apiType === 'mcp' && dynamicOperation === 'listMcpTools'; const invokeParameters = this._getInvokeParameters(parameters, dynamicState); @@ -69,7 +71,55 @@ export class ConsumptionConnectorService extends BaseConnectorService { }); } - const uri = `${connectionId}/dynamicList`; + // MCP-specific: use listMcpTools endpoint instead of dynamicList + if (isMcpConnection) { + const { workflowReferenceId } = this.options; + const uri = `${workflowReferenceId}/listMcpTools`; + + // Get the connection data from ConnectionService + let connection: any; + try { + connection = connectionId ? await ConnectionService().getConnection(connectionId) : undefined; + } catch { + // Connection may not be in cache/API Hub for built-in MCP + } + + let content: any = {}; + if (connection?.properties?.parameterValues) { + // Built-in MCP connection — send connection info in the expected format + const { mcpServerUrl, authenticationType, ...authParams } = connection.properties.parameterValues; + content = { + connection: { + inputs: { + Connection: { + McpServerUrl: mcpServerUrl, + Authentication: authenticationType || 'None', + ...authParams, + }, + }, + type: 'McpClientTool', + kind: 'BuiltIn', + }, + }; + } + + const mcpToolsResponse = await httpClient.post({ + uri, + queryParameters: { 'api-version': '2018-07-01-preview' }, + content, + }); + const tools = this._getResponseFromDynamicApi(mcpToolsResponse, uri); + + return (tools ?? []).map((tool: any) => ({ + value: tool.name, + displayName: tool.name, + description: tool.description, + })); + } + + // Regular dynamic list for non-MCP connections + const resolvedConnectionId = this._resolveConnectionId(connectionId); + const uri = `${resolvedConnectionId}/dynamicList`; const response = await httpClient.post({ uri, queryParameters: { 'api-version': apiVersion }, @@ -110,7 +160,8 @@ export class ConsumptionConnectorService extends BaseConnectorService { }); } - const uri = `${connectionId}/dynamicProperties`; + const resolvedConnectionId = this._resolveConnectionId(connectionId); + const uri = `${resolvedConnectionId}/dynamicProperties`; const response = await httpClient.post({ uri, queryParameters: { 'api-version': apiVersion }, @@ -136,7 +187,8 @@ export class ConsumptionConnectorService extends BaseConnectorService { const { apiVersion, httpClient } = this.options; const { dynamicState, selectionState } = dynamicExtension; - const uri = `${connectionId}/dynamicTree`; + const resolvedConnectionId = this._resolveConnectionId(connectionId); + const uri = `${resolvedConnectionId}/dynamicTree`; const response = await httpClient.post({ uri, queryParameters: { 'api-version': apiVersion }, @@ -156,6 +208,13 @@ export class ConsumptionConnectorService extends BaseConnectorService { })); } + private _resolveConnectionId(connectionId: string | undefined): string | undefined { + if (connectionId && !connectionId.startsWith('/subscriptions/')) { + return `${this.options.workflowReferenceId}/${connectionId}`; + } + return connectionId; + } + private _getPropertiesIfNeeded(isManagedIdentityConnection?: boolean): | { workflowReference: { name: string; id: string; type: string }; @@ -174,4 +233,32 @@ export class ConsumptionConnectorService extends BaseConnectorService { return undefined; } + + private _buildMcpAuthentication(connectionProperties: Record): Record | undefined { + const authType = connectionProperties['authenticationType']; + if (!authType || authType === 'None') { + return undefined; + } + + const authentication: Record = { type: authType }; + if (authType === 'ApiKey') { + authentication['value'] = connectionProperties['key']; + authentication['name'] = connectionProperties['keyHeaderName']; + authentication['in'] = 'header'; + } else if (authType === 'Basic') { + authentication['username'] = connectionProperties['username']; + authentication['password'] = connectionProperties['password']; + } else if (authType === 'ActiveDirectoryOAuth') { + authentication['tenant'] = connectionProperties['tenant']; + authentication['clientId'] = connectionProperties['clientId']; + authentication['secret'] = connectionProperties['secret']; + authentication['authority'] = connectionProperties['authority']; + authentication['audience'] = connectionProperties['audience']; + } else if (authType === 'ClientCertificate') { + authentication['pfx'] = connectionProperties['pfx']; + authentication['password'] = connectionProperties['password']; + } + + return authentication; + } } diff --git a/libs/logic-apps-shared/src/designer-client-services/lib/consumption/manifests/builtinmcpclient.ts b/libs/logic-apps-shared/src/designer-client-services/lib/consumption/manifests/builtinmcpclient.ts new file mode 100644 index 00000000000..49e3ea89644 --- /dev/null +++ b/libs/logic-apps-shared/src/designer-client-services/lib/consumption/manifests/builtinmcpclient.ts @@ -0,0 +1,69 @@ +import type { OperationManifest } from '../../../../utils/src'; +import mcpclientconnector from './mcpclientconnector'; + +export default { + properties: { + iconUri: mcpclientconnector.properties.iconUri, + brandColor: '#000000', + description: 'Uses an MCP server', + inputsBindingMode: 'untyped', + inputs: { + type: 'object', + properties: { + headers: { + type: 'object', + title: 'Headers', + description: 'Enter JSON object of request headers', + 'x-ms-editor': 'dictionary', + 'x-ms-editor-options': { + valueType: 'string', + }, + }, + allowedTools: { + type: 'array', + items: { + type: 'string', + }, + title: 'Allowed tools', + 'x-ms-editor': 'combobox', + 'x-ms-editor-options': { + multiSelect: true, + titleSeparator: ',', + serialization: { + valueType: 'array', + }, + }, + 'x-ms-dynamic-list': { + dynamicState: { + apiType: 'mcp', + operationId: 'listMcpTools', + }, + }, + }, + }, + }, + outputsBindingMode: 'untyped', + outputs: { + type: 'object', + properties: {}, + }, + isOutputsOptional: false, + inputsLocation: ['inputs', 'parameters'], + isInputsOptional: false, + + runAfter: { + type: 'notsupported', + }, + + connection: { + required: true, + type: 'mcp', + disableAutoSelection: true, + }, + connectionReference: { + referenceKeyFormat: 'mcpconnection', + }, + + connector: mcpclientconnector, + }, +} as OperationManifest; diff --git a/libs/logic-apps-shared/src/designer-client-services/lib/consumption/manifests/mcpclientconnector.ts b/libs/logic-apps-shared/src/designer-client-services/lib/consumption/manifests/mcpclientconnector.ts new file mode 100644 index 00000000000..ef7f83f241d --- /dev/null +++ b/libs/logic-apps-shared/src/designer-client-services/lib/consumption/manifests/mcpclientconnector.ts @@ -0,0 +1,345 @@ +import type { Connector } from '../../../../utils/src'; + +export default { + type: 'McpClient', + name: 'mcpclient', + id: 'connectionProviders/mcpclient', + properties: { + displayName: 'MCP Client', + iconUri: + 'data:image/svg+xml,%3Csvg%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%3Crect%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22%23242424%22%2F%3E%0A%3Cg%20clip-path%3D%22url(%23clip0_1258_56822)%22%3E%0A%3Crect%20width%3D%2220%22%20height%3D%2220%22%20transform%3D%22translate(2%202)%22%20fill%3D%22%23242424%22%2F%3E%0A%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M15.0733%203.9524C14.6707%203.56054%2014.131%203.34127%2013.5691%203.34127C13.0073%203.34127%2012.4676%203.56054%2012.065%203.9524L4.04331%2011.8191C3.90907%2011.9495%203.72926%2012.0225%203.54206%2012.0225C3.35486%2012.0225%203.17504%2011.9495%203.04081%2011.8191C2.97509%2011.7552%202.92285%2011.6787%202.88718%2011.5943C2.85151%2011.5098%202.83313%2011.4191%202.83313%2011.3274C2.83313%2011.2357%202.85151%2011.145%202.88718%2011.0605C2.92285%2010.9761%202.97509%2010.8996%203.04081%2010.8357L11.0625%202.96907C11.7335%202.31606%2012.6328%201.95068%2013.5691%201.95068C14.5055%201.95068%2015.4048%202.31606%2016.0758%202.96907C16.4641%203.34665%2016.7574%203.81081%2016.9318%204.32356C17.1062%204.8363%2017.1567%205.38306%2017.0791%205.91907C17.6223%205.84182%2018.1759%205.89031%2018.6973%206.06079C19.2187%206.23128%2019.6941%206.51921%2020.0866%206.9024L20.1283%206.94407C20.4569%207.26363%2020.7181%207.64584%2020.8965%208.06808C21.0748%208.49032%2021.1667%208.94404%2021.1667%209.4024C21.1667%209.86077%2021.0748%2010.3145%2020.8965%2010.7367C20.7181%2011.159%2020.4569%2011.5412%2020.1283%2011.8607L12.8733%2018.9749C12.8514%2018.9962%2012.834%2019.0216%2012.8221%2019.0498C12.8102%2019.0779%2012.8041%2019.1081%2012.8041%2019.1387C12.8041%2019.1692%2012.8102%2019.1994%2012.8221%2019.2275C12.834%2019.2557%2012.8514%2019.2811%2012.8733%2019.3024L14.3633%2020.7641C14.429%2020.828%2014.4813%2020.9044%2014.5169%2020.9889C14.5526%2021.0733%2014.571%2021.1641%2014.571%2021.2557C14.571%2021.3474%2014.5526%2021.4381%2014.5169%2021.5226C14.4813%2021.607%2014.429%2021.6835%2014.3633%2021.7474C14.2291%2021.8779%2014.0493%2021.9509%2013.8621%2021.9509C13.6749%2021.9509%2013.495%2021.8779%2013.3608%2021.7474L11.8708%2020.2866C11.7173%2020.1374%2011.5953%2019.9591%2011.512%2019.762C11.4287%2019.5649%2011.3858%2019.353%2011.3858%2019.1391C11.3858%2018.9251%2011.4287%2018.7133%2011.512%2018.5162C11.5953%2018.3191%2011.7173%2018.1407%2011.8708%2017.9916L19.1258%2010.8766C19.3229%2010.6848%2019.4795%2010.4554%2019.5864%2010.2021C19.6934%209.94875%2019.7485%209.67655%2019.7485%209.40157C19.7485%209.12658%2019.6934%208.85438%2019.5864%208.60104C19.4795%208.34771%2019.3229%208.11837%2019.1258%207.92657L19.0841%207.88573C18.6819%207.49428%2018.143%207.27504%2017.5817%207.27457C17.0205%207.27411%2016.4812%207.49245%2016.0783%207.88323L10.1016%2013.7449L10.1%2013.7466L10.0183%2013.8274C9.88404%2013.9581%209.70404%2014.0313%209.51664%2014.0313C9.32925%2014.0313%209.14925%2013.9581%209.01498%2013.8274C8.94926%2013.7635%208.89702%2013.687%208.86135%2013.6026C8.82568%2013.5182%208.8073%2013.4274%208.8073%2013.3357C8.8073%2013.2441%208.82568%2013.1533%208.86135%2013.0689C8.89702%2012.9844%208.94926%2012.908%209.01498%2012.8441L15.0758%206.8999C15.2723%206.70797%2015.4284%206.47866%2015.5349%206.22546C15.6414%205.97226%2015.6962%205.70031%2015.696%205.42562C15.6957%205.15094%2015.6405%204.87908%2015.5336%204.62606C15.4266%204.37305%2015.2701%204.14399%2015.0733%203.9524Z%22%20fill%3D%22white%22%2F%3E%0A%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M14.0708%205.91914C14.1365%205.85522%2014.1887%205.77878%2014.2244%205.69433C14.2601%205.60989%2014.2785%205.51914%2014.2785%205.42747C14.2785%205.3358%2014.2601%205.24505%2014.2244%205.1606C14.1887%205.07616%2014.1365%204.99972%2014.0708%204.9358C13.9365%204.80508%2013.7565%204.73193%2013.5691%204.73193C13.3817%204.73193%2013.2017%204.80508%2013.0675%204.9358L7.13495%2010.7541C6.80636%2011.0737%206.54516%2011.4559%206.36681%2011.8781C6.18845%2012.3004%206.09656%2012.7541%206.09656%2013.2125C6.09656%2013.6708%206.18845%2014.1245%206.36681%2014.5468C6.54516%2014.969%206.80636%2015.3512%207.13495%2015.6708C7.80607%2016.3236%208.70538%2016.6889%209.64162%2016.6889C10.5779%2016.6889%2011.4772%2016.3236%2012.1483%2015.6708L18.0816%209.85247C18.1473%209.78856%2018.1996%209.71212%2018.2352%209.62767C18.2709%209.54322%2018.2893%209.45248%2018.2893%209.3608C18.2893%209.26913%2018.2709%209.17839%2018.2352%209.09394C18.1996%209.00949%2018.1473%208.93305%2018.0816%208.86914C17.9473%208.73841%2017.7674%208.66527%2017.58%208.66527C17.3926%208.66527%2017.2126%208.73841%2017.0783%208.86914L11.1458%2014.6875C10.7431%2015.0793%2010.2035%2015.2986%209.64162%2015.2986C9.07977%2015.2986%208.5401%2015.0793%208.13745%2014.6875C7.9404%2014.4957%207.78377%2014.2663%207.67683%2014.013C7.56988%2013.7597%207.51478%2013.4875%207.51478%2013.2125C7.51478%2012.9375%207.56988%2012.6653%207.67683%2012.4119C7.78377%2012.1586%207.9404%2011.9293%208.13745%2011.7375L14.0708%205.91914Z%22%20fill%3D%22white%22%2F%3E%0A%3C%2Fg%3E%0A%3Cdefs%3E%0A%3CclipPath%20id%3D%22clip0_1258_56822%22%3E%0A%3Crect%20width%3D%2220%22%20height%3D%2220%22%20fill%3D%22white%22%20transform%3D%22translate(2%202)%22%2F%3E%0A%3C%2FclipPath%3E%0A%3C%2Fdefs%3E%0A%3C%2Fsvg%3E%0A', + brandColor: '#000000', + description: 'Easily integrate cutting-edge artificial intelligence capabilities into your workflows', + capabilities: ['actions', 'builtin'], + connectionMode: 'mcp', + connectionParameterSets: { + uiDefinition: { + displayName: 'Authentication type', + description: 'The authentication type to use.', + }, + values: [ + { + name: 'None', + parameters: { + serverUrl: { + type: 'string', + uiDefinition: { + displayName: 'Server URL', + constraints: { + required: 'true', + }, + description: 'Server URL', + }, + }, + }, + uiDefinition: { + displayName: 'None', + description: 'None', + }, + }, + { + name: 'Basic', + parameters: { + serverUrl: { + type: 'string', + uiDefinition: { + displayName: 'Server URL', + constraints: { + required: 'true', + }, + description: 'Server URL', + }, + }, + username: { + type: 'string', + uiDefinition: { + displayName: 'Username', + constraints: { + propertyPath: ['authentication'], + required: 'true', + }, + description: 'Username', + }, + }, + password: { + type: 'securestring', + uiDefinition: { + displayName: 'Password', + constraints: { + propertyPath: ['authentication'], + required: 'true', + }, + description: 'Password', + }, + }, + }, + uiDefinition: { + displayName: 'Basic', + }, + }, + { + name: 'ClientCertificate', + parameters: { + serverUrl: { + type: 'string', + uiDefinition: { + displayName: 'Server URL', + constraints: { + required: 'true', + }, + description: 'Server URL', + }, + }, + pfx: { + type: 'securestring', + uiDefinition: { + displayName: 'Pfx', + constraints: { + required: 'true', + propertyPath: ['authentication'], + }, + description: 'Client Certificate pfx', + }, + }, + password: { + type: 'securestring', + uiDefinition: { + displayName: 'Password', + constraints: { + propertyPath: ['authentication'], + }, + description: 'Client Certificate password', + }, + }, + }, + uiDefinition: { + displayName: 'Client certificate', + }, + }, + { + name: 'ActiveDirectoryOAuth', + parameters: { + serverUrl: { + type: 'string', + uiDefinition: { + displayName: 'Server URL', + constraints: { + required: 'true', + }, + description: 'Server URL', + }, + }, + authority: { + type: 'string', + uiDefinition: { + displayName: 'Authority', + constraints: { + propertyPath: ['authentication'], + }, + description: 'Active Directory authority', + }, + }, + tenant: { + type: 'string', + uiDefinition: { + displayName: 'Tenant', + constraints: { + required: 'true', + propertyPath: ['authentication'], + }, + description: 'Active Directory tenant', + }, + }, + audience: { + type: 'string', + parameterSource: 'AppConfiguration', + uiDefinition: { + displayName: 'Audience', + constraints: { + required: 'true', + propertyPath: ['authentication'], + }, + description: 'Active Directory audience', + }, + }, + credentialType: { + type: 'string', + uiDefinition: { + displayName: 'Credential type', + constraints: { + propertyPath: ['authentication'], + allowedValues: [ + { + text: 'Certificate', + value: 'Certificate', + }, + { + text: 'Secret', + value: 'Secret', + }, + ], + }, + description: 'Active Directory credential type', + }, + }, + clientId: { + type: 'string', + uiDefinition: { + displayName: 'Client ID', + constraints: { + required: 'true', + propertyPath: ['authentication'], + }, + description: 'Active Directory client ID', + }, + }, + secret: { + type: 'securestring', + uiDefinition: { + displayName: 'Client secret', + constraints: { + required: 'true', + propertyPath: ['authentication'], + dependentParameter: { + parameter: 'CredentialType', + value: 'Secret', + }, + }, + description: 'Active Directory client secret', + }, + }, + pfx: { + type: 'securestring', + uiDefinition: { + displayName: 'Pfx', + constraints: { + required: 'true', + propertyPath: ['authentication'], + dependentParameter: { + parameter: 'CredentialType', + value: 'Certificate', + }, + }, + description: 'Active Directory pfx', + }, + }, + password: { + type: 'securestring', + uiDefinition: { + displayName: 'Password', + constraints: { + required: 'true', + propertyPath: ['authentication'], + dependentParameter: { + parameter: 'CredentialType', + value: 'Certificate', + }, + }, + description: 'Active Directory password', + }, + }, + }, + uiDefinition: { + displayName: 'Active Directory OAuth', + }, + }, + { + name: 'Raw', + parameters: { + serverUrl: { + type: 'string', + uiDefinition: { + displayName: 'Server URL', + constraints: { + required: 'true', + }, + description: 'Server URL', + }, + }, + value: { + type: 'string', + uiDefinition: { + displayName: 'Value', + constraints: { + propertyPath: ['authentication'], + required: 'true', + }, + }, + }, + }, + uiDefinition: { + displayName: 'Raw', + }, + }, + { + name: 'Key', + parameters: { + serverUrl: { + type: 'string', + uiDefinition: { + displayName: 'Server URL', + constraints: { + required: 'true', + }, + description: 'Server URL', + }, + }, + key: { + type: 'securestring', + uiDefinition: { + displayName: 'Key', + constraints: { + required: 'true', + propertyPath: ['authentication'], + }, + description: 'Key', + }, + }, + keyHeaderName: { + type: 'string', + uiDefinition: { + displayName: 'Key Header Name', + constraints: { + propertyPath: ['authentication'], + }, + description: 'Key header name', + }, + }, + }, + uiDefinition: { + displayName: 'Key', + }, + }, + { + name: 'ManagedServiceIdentity', + parameters: { + serverUrl: { + type: 'string', + uiDefinition: { + displayName: 'Server URL', + constraints: { + required: 'true', + }, + description: 'Server URL', + }, + }, + audience: { + type: 'string', + uiDefinition: { + displayName: 'Audience', + constraints: { + required: 'true', + propertyPath: ['authentication'], + }, + description: 'The audience', + }, + }, + }, + uiDefinition: { + displayName: 'Managed identity', + }, + }, + ], + }, + }, +} as Connector; diff --git a/libs/logic-apps-shared/src/designer-client-services/lib/consumption/operationmanifest.ts b/libs/logic-apps-shared/src/designer-client-services/lib/consumption/operationmanifest.ts index 5a75360ddb1..178f186b001 100644 --- a/libs/logic-apps-shared/src/designer-client-services/lib/consumption/operationmanifest.ts +++ b/libs/logic-apps-shared/src/designer-client-services/lib/consumption/operationmanifest.ts @@ -7,6 +7,7 @@ import { agentType, getBuiltInOperationInfo, isBuiltInOperation, + mcpclientConnectorId, supportedBaseManifestObjects, supportedBaseManifestTypes, } from '../base/operationmanifest'; @@ -39,6 +40,8 @@ import { import { getBuiltInConnectorsInConsumption } from './search'; import agentloop from './manifests/agentloop'; import a2arequestManifest from './manifests/a2arequest'; +import builtinMcpClientManifest from './manifests/builtinmcpclient'; +import mcpclientconnector from './manifests/mcpclientconnector'; interface ConsumptionOperationManifestServiceOptions extends BaseOperationManifestServiceOptions { subscriptionId: string; @@ -62,6 +65,19 @@ export class ConsumptionOperationManifestService extends BaseOperationManifestSe result[connector.id.toLowerCase()] = connector; return result; }, {}); + + this.allBuiltInConnectors[mcpclientconnector.id.toLowerCase()] = mcpclientconnector; + } + + override isSupported(operationType?: string, operationKind?: string): boolean { + if (operationType?.toLowerCase() === 'mcpclienttool' && operationKind?.toLowerCase() === 'builtin') { + return true; + } + const { supportedTypes } = this.options; + const normalizedOperationType = operationType?.toLowerCase() ?? ''; + return supportedTypes + ? supportedTypes.indexOf(normalizedOperationType) > -1 + : supportedConsumptionManifestTypes.indexOf(normalizedOperationType) > -1; } override async getOperationInfo(definition: any, isTrigger: boolean): Promise { @@ -69,6 +85,12 @@ export class ConsumptionOperationManifestService extends BaseOperationManifestSe const normalizedOperationType = definition.type?.toLowerCase(); switch (normalizedOperationType) { + case 'mcpclienttool': + return { + connectorId: mcpclientConnectorId, + operationId: 'nativemcpclient', + }; + case 'workflow': return { connectorId: invokeWorkflowGroup.id, @@ -102,15 +124,11 @@ export class ConsumptionOperationManifestService extends BaseOperationManifestSe throw new UnsupportedException(`Operation type: ${definition.type} does not support manifest.`); } - override isSupported(operationType?: string, _operationKind?: string): boolean { - const { supportedTypes } = this.options; - const normalizedOperationType = operationType?.toLowerCase() ?? ''; - return supportedTypes - ? supportedTypes.indexOf(normalizedOperationType) > -1 - : supportedConsumptionManifestTypes.indexOf(normalizedOperationType) > -1; - } - override async getOperationManifest(connectorId: string, operationId: string): Promise { + if (connectorId?.toLowerCase() === mcpclientConnectorId.toLowerCase() && operationId === 'nativemcpclient') { + return builtinMcpClientManifest; + } + const supportedManifest = supportedConsumptionManifestObjects.get(operationId); if (supportedManifest) { @@ -160,6 +178,17 @@ export class ConsumptionOperationManifestService extends BaseOperationManifestSe } override async getOperation(_connectorId: string, operationId: string, _useCachedData = false): Promise { + if (_connectorId?.toLowerCase() === mcpclientConnectorId.toLowerCase() && operationId === 'nativemcpclient') { + return { + properties: { + connector: { properties: { displayName: builtinMcpClientManifest.properties.connector?.properties.displayName } }, + brandColor: builtinMcpClientManifest.properties.brandColor, + description: builtinMcpClientManifest.properties.description, + iconUri: builtinMcpClientManifest.properties.iconUri, + }, + }; + } + const supportedManifest = supportedConsumptionManifestObjects.get(operationId); if (supportedManifest) {