Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions libs/designer-v2/src/lib/core/actions/bjsworkflow/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.
Expand All @@ -593,11 +601,18 @@ const serializeBuiltInMcpOperation = async (rootState: RootState, nodeId: string

let inputs: Record<string, any> | undefined;
if (mcpServerUrl) {
const connectionBlock: Record<string, any> = {
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) {
Expand Down
23 changes: 19 additions & 4 deletions libs/designer/src/lib/core/actions/bjsworkflow/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.
Expand All @@ -570,11 +578,18 @@ const serializeBuiltInMcpOperation = async (rootState: RootState, nodeId: string

let inputs: Record<string, any> | undefined;
if (mcpServerUrl) {
const connectionBlock: Record<string, any> = {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Connector> = {
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', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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']);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export class ConsumptionConnectionService extends BaseConnectionService {
'tenant',
'authority',
'audience',
'identity',
'pfx',
'identity',
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ export default {
constraints: {
required: 'false',
editor: 'identitypicker',
propertyPath: ['authentication'],
},
description: 'The managed identity to use for authentication',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ export default {
constraints: {
required: 'false',
editor: 'identitypicker',
propertyPath: ['authentication'],
},
description: 'The managed identity to use for authentication',
},
Expand Down
Loading