diff --git a/libs/designer-v2/src/lib/core/actions/bjsworkflow/serializer.ts b/libs/designer-v2/src/lib/core/actions/bjsworkflow/serializer.ts index 8efb4812f20..4bf8a7e8244 100644 --- a/libs/designer-v2/src/lib/core/actions/bjsworkflow/serializer.ts +++ b/libs/designer-v2/src/lib/core/actions/bjsworkflow/serializer.ts @@ -573,6 +573,8 @@ const serializeBuiltInMcpOperation = async (rootState: RootState, nodeId: string let mcpServerUrl = existingConnectionInput?.McpServerUrl ?? ''; let authenticationType = existingConnectionInput?.Authentication ?? 'None'; + let authIdentity: string | undefined = existingConnectionInput?.Identity; + let authAudience: string | undefined = existingConnectionInput?.Audience; if (connectionId) { try { @@ -581,6 +583,12 @@ const serializeBuiltInMcpOperation = async (rootState: RootState, nodeId: string if (parameterValues) { mcpServerUrl = parameterValues.mcpServerUrl ?? mcpServerUrl; authenticationType = parameterValues.authenticationType ?? authenticationType; + authIdentity = Object.prototype.hasOwnProperty.call(parameterValues, 'identity') + ? (parameterValues.identity ?? undefined) + : undefined; + authAudience = Object.prototype.hasOwnProperty.call(parameterValues, 'audience') + ? (parameterValues.audience ?? undefined) + : undefined; } } catch { // Keep existing values when connection lookup fails. @@ -593,11 +601,18 @@ const serializeBuiltInMcpOperation = async (rootState: RootState, nodeId: string let inputs: Record | undefined; if (mcpServerUrl) { + const connectionBlock: Record = { + Authentication: authenticationType, + McpServerUrl: mcpServerUrl, + }; + if (authIdentity) { + connectionBlock.Identity = authIdentity; + } + if (authAudience) { + connectionBlock.Audience = authAudience; + } inputs = { - Connection: { - Authentication: authenticationType, - McpServerUrl: mcpServerUrl, - }, + Connection: connectionBlock, ...(hasParameters ? { parameters: { ...inputParameters.parameters } } : {}), }; } else if (hasParameters) { diff --git a/libs/designer/src/lib/core/actions/bjsworkflow/serializer.ts b/libs/designer/src/lib/core/actions/bjsworkflow/serializer.ts index a24a8556a1f..dded3974dc8 100644 --- a/libs/designer/src/lib/core/actions/bjsworkflow/serializer.ts +++ b/libs/designer/src/lib/core/actions/bjsworkflow/serializer.ts @@ -550,6 +550,8 @@ const serializeBuiltInMcpOperation = async (rootState: RootState, nodeId: string let mcpServerUrl = existingConnectionInput?.McpServerUrl ?? ''; let authenticationType = existingConnectionInput?.Authentication ?? 'None'; + let authIdentity: string | undefined = existingConnectionInput?.Identity; + let authAudience: string | undefined = existingConnectionInput?.Audience; if (connectionId) { try { @@ -558,6 +560,12 @@ const serializeBuiltInMcpOperation = async (rootState: RootState, nodeId: string if (parameterValues) { mcpServerUrl = parameterValues.mcpServerUrl ?? mcpServerUrl; authenticationType = parameterValues.authenticationType ?? authenticationType; + authIdentity = Object.prototype.hasOwnProperty.call(parameterValues, 'identity') + ? (parameterValues.identity ?? undefined) + : undefined; + authAudience = Object.prototype.hasOwnProperty.call(parameterValues, 'audience') + ? (parameterValues.audience ?? undefined) + : undefined; } } catch { // Keep existing values when connection lookup fails. @@ -570,11 +578,18 @@ const serializeBuiltInMcpOperation = async (rootState: RootState, nodeId: string let inputs: Record | undefined; if (mcpServerUrl) { + const connectionBlock: Record = { + Authentication: authenticationType, + McpServerUrl: mcpServerUrl, + }; + if (authIdentity) { + connectionBlock.Identity = authIdentity; + } + if (authAudience) { + connectionBlock.Audience = authAudience; + } inputs = { - Connection: { - Authentication: authenticationType, - McpServerUrl: mcpServerUrl, - }, + Connection: connectionBlock, ...(hasParameters ? { parameters: { ...inputParameters.parameters } } : {}), }; } else if (hasParameters) { 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 index d66c4addb69..50f8386207a 100644 --- 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 @@ -311,6 +311,106 @@ describe('ConsumptionConnectionService', () => { expect(result.authParams.unknownParam).toBeUndefined(); expect(result.authParams.anotherUnknown).toBeUndefined(); }); + + it('should extract ManagedServiceIdentity parameters including identity', () => { + const result = (service as any).extractAuthParameters({ + name: 'ManagedServiceIdentity', + values: { + identity: { + value: '/subscriptions/sub-id/resourceGroups/rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity', + }, + audience: { value: 'api://my-mcp-audience' }, + }, + }); + + expect(result.authenticationType).toBe('ManagedServiceIdentity'); + expect(result.authParams.identity).toBe( + '/subscriptions/sub-id/resourceGroups/rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity' + ); + expect(result.authParams.audience).toBe('api://my-mcp-audience'); + }); + + it('should extract ManagedServiceIdentity with audience only (system-assigned identity)', () => { + const result = (service as any).extractAuthParameters({ + name: 'ManagedServiceIdentity', + values: { + audience: { value: 'api://my-mcp-audience' }, + }, + }); + + expect(result.authenticationType).toBe('ManagedServiceIdentity'); + expect(result.authParams.identity).toBeUndefined(); + expect(result.authParams.audience).toBe('api://my-mcp-audience'); + }); + }); + + describe('createBuiltInMcpConnection with ManagedServiceIdentity', () => { + const mcpConnector: 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', + }, + }, + }; + + it('should include identity and audience in parameterValues for MSI auth', async () => { + const connectionInfo: ConnectionCreationInfo = { + displayName: 'msi-mcp-connection', + connectionParameters: { + serverUrl: { value: 'https://icm-mcp-ppe.azure-api.net/v1/' }, + }, + connectionParametersSet: { + name: 'ManagedServiceIdentity', + values: { + identity: { + value: + '/subscriptions/sub-id/resourceGroups/rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/XiaoyuAgenticWorkflow', + }, + audience: { value: 'api://icmmcpapi-ppe' }, + }, + }, + }; + + const result = await service.createConnection('test-msi-conn', mcpConnector as Connector, connectionInfo); + + expect(result).toBeDefined(); + expect((result.properties as any).parameterValues.authenticationType).toBe('ManagedServiceIdentity'); + expect((result.properties as any).parameterValues.identity).toBe( + '/subscriptions/sub-id/resourceGroups/rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/XiaoyuAgenticWorkflow' + ); + expect((result.properties as any).parameterValues.audience).toBe('api://icmmcpapi-ppe'); + expect((result.properties as any).parameterValues.mcpServerUrl).toBe('https://icm-mcp-ppe.azure-api.net/v1/'); + }); + + it('should store audience without identity for system-assigned MSI', async () => { + const connectionInfo: ConnectionCreationInfo = { + displayName: 'msi-system-conn', + connectionParameters: { + serverUrl: { value: 'https://mcp-server.example.com' }, + }, + connectionParametersSet: { + name: 'ManagedServiceIdentity', + values: { + audience: { value: 'api://my-audience' }, + }, + }, + }; + + const result = await service.createConnection('test-system-msi', mcpConnector as Connector, connectionInfo); + + expect((result.properties as any).parameterValues.authenticationType).toBe('ManagedServiceIdentity'); + expect((result.properties as any).parameterValues.audience).toBe('api://my-audience'); + expect((result.properties as any).parameterValues.identity).toBeUndefined(); + }); }); describe('getConnector', () => { 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 f2920c6652e..f760db0e9a0 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 @@ -1,5 +1,6 @@ import { describe, test, expect, beforeEach, vi } from 'vitest'; import { ConsumptionOperationManifestService } from '../operationmanifest'; +import mcpclientconnector from '../manifests/mcpclientconnector'; import type { IHttpClient } from '../../httpClient'; describe('ConsumptionOperationManifestService', () => { @@ -147,4 +148,32 @@ describe('ConsumptionOperationManifestService', () => { expect(result.properties).toBeDefined(); }); }); + + describe('MCP connector ManagedServiceIdentity parameters', () => { + test('should define identity parameter in ManagedServiceIdentity parameter set', () => { + const paramSets = mcpclientconnector.properties.connectionParameterSets?.values; + const msiSet = paramSets?.find((p: any) => p.name === 'ManagedServiceIdentity'); + + expect(msiSet).toBeDefined(); + expect(msiSet?.parameters.identity).toBeDefined(); + expect(msiSet?.parameters.identity.type).toBe('string'); + expect(msiSet?.parameters.identity.uiDefinition.displayName).toBe('Managed identity'); + }); + + test('should define audience parameter in ManagedServiceIdentity parameter set', () => { + const paramSets = mcpclientconnector.properties.connectionParameterSets?.values; + const msiSet = paramSets?.find((p: any) => p.name === 'ManagedServiceIdentity'); + + expect(msiSet).toBeDefined(); + expect(msiSet?.parameters.audience).toBeDefined(); + expect(msiSet?.parameters.audience.uiDefinition.constraints.required).toBe('true'); + }); + + test('should mark identity as authentication property path', () => { + const paramSets = mcpclientconnector.properties.connectionParameterSets?.values; + const msiSet = paramSets?.find((p: any) => p.name === 'ManagedServiceIdentity'); + + expect(msiSet?.parameters.identity.uiDefinition.constraints.propertyPath).toEqual(['authentication']); + }); + }); }); 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 5458b523233..f952b879198 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 @@ -51,6 +51,7 @@ export class ConsumptionConnectionService extends BaseConnectionService { 'tenant', 'authority', 'audience', + 'identity', 'pfx', 'identity', ]; 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 index 01a67ce8ed2..ddcaa61fea6 100644 --- 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 @@ -330,6 +330,7 @@ export default { constraints: { required: 'false', editor: 'identitypicker', + propertyPath: ['authentication'], }, description: 'The managed identity to use for authentication', }, diff --git a/libs/logic-apps-shared/src/designer-client-services/lib/standard/connection.ts b/libs/logic-apps-shared/src/designer-client-services/lib/standard/connection.ts index bd718f593e6..254122b2ed1 100644 --- a/libs/logic-apps-shared/src/designer-client-services/lib/standard/connection.ts +++ b/libs/logic-apps-shared/src/designer-client-services/lib/standard/connection.ts @@ -1096,6 +1096,7 @@ function convertToMcpConnectionsData( processValue(connectionParametersSet.values, 'authority', false); processValue(connectionParametersSet.values, 'tenant', false); processValue(connectionParametersSet.values, 'audience', false); + processValue(connectionParametersSet.values, 'identity', false); processValue(connectionParametersSet.values, 'clientId', false); processValue(connectionParametersSet.values, 'secret', true); processValue(connectionParametersSet.values, 'value', true); diff --git a/libs/logic-apps-shared/src/designer-client-services/lib/standard/manifest/mcpclientconnector.ts b/libs/logic-apps-shared/src/designer-client-services/lib/standard/manifest/mcpclientconnector.ts index 4a4f89aefa0..229ecce401f 100644 --- a/libs/logic-apps-shared/src/designer-client-services/lib/standard/manifest/mcpclientconnector.ts +++ b/libs/logic-apps-shared/src/designer-client-services/lib/standard/manifest/mcpclientconnector.ts @@ -330,6 +330,7 @@ export default { constraints: { required: 'false', editor: 'identitypicker', + propertyPath: ['authentication'], }, description: 'The managed identity to use for authentication', },