diff --git a/apps/vben5/packages/@abp/ai-management/src/components/tools/AIToolDefinitionModal.vue b/apps/vben5/packages/@abp/ai-management/src/components/tools/AIToolDefinitionModal.vue index 3e885c2ff..cde5d0c58 100644 --- a/apps/vben5/packages/@abp/ai-management/src/components/tools/AIToolDefinitionModal.vue +++ b/apps/vben5/packages/@abp/ai-management/src/components/tools/AIToolDefinitionModal.vue @@ -1,6 +1,4 @@ + + + + diff --git a/apps/vben5/packages/@abp/ai-management/src/types/tools.ts b/apps/vben5/packages/@abp/ai-management/src/types/tools.ts index b7504e566..9ddcf5761 100644 --- a/apps/vben5/packages/@abp/ai-management/src/types/tools.ts +++ b/apps/vben5/packages/@abp/ai-management/src/types/tools.ts @@ -39,6 +39,7 @@ interface AIToolDefinitionRecordGetPagedListInput extends PagedAndSortedResultRe } interface AIToolPropertyDescriptorDto { + dependencies: NameValue[]; description?: string; displayName: string; name: string; @@ -57,5 +58,6 @@ export type { AIToolDefinitionRecordDto, AIToolDefinitionRecordGetPagedListInput, AIToolDefinitionRecordUpdateDto, + AIToolPropertyDescriptorDto, AIToolProviderDto, }; diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/DefaultMcpAuthorizationCodeProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/DefaultMcpAuthorizationCodeProvider.cs new file mode 100644 index 000000000..102377f15 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/DefaultMcpAuthorizationCodeProvider.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using System.Web; +using Volo.Abp.DependencyInjection; + +namespace LINGYUN.Abp.AI.Tools.Mcp; + +[Dependency(TryRegister = true)] +public class DefaultMcpAuthorizationCodeProvider : IMcpAuthorizationCodeProvider, ISingletonDependency +{ + public async virtual Task HandleAsync(McpAuthorizationCodeHandleContext context) + { + using var redirectResponse = await context.HttpClient.GetAsync(context.AuthorizationUrl, context.CancellationToken); + var location = redirectResponse.Headers.Location; + + if (location is not null && !string.IsNullOrEmpty(location.Query)) + { + // Parse query string to extract "code" parameter + var query = location.Query.TrimStart('?'); + foreach (var pair in query.Split('&')) + { + var parts = pair.Split('=', 2); + if (parts.Length == 2 && parts[0] == "code") + { + return HttpUtility.UrlDecode(parts[1]); + } + } + } + + return null; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/IMcpAuthorizationCodeProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/IMcpAuthorizationCodeProvider.cs new file mode 100644 index 000000000..132e71fb3 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/IMcpAuthorizationCodeProvider.cs @@ -0,0 +1,7 @@ +using System.Threading.Tasks; + +namespace LINGYUN.Abp.AI.Tools.Mcp; +public interface IMcpAuthorizationCodeProvider +{ + Task HandleAsync(McpAuthorizationCodeHandleContext context); +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/Localization/Resources/en.json b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/Localization/Resources/en.json index 668fef84b..82df26809 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/Localization/Resources/en.json +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/Localization/Resources/en.json @@ -9,6 +9,14 @@ "McpAITool:ConnectionTimeoutDesc": "The timeout used to establish the initial connection to the SSE server. The default is 30 seconds.", "McpAITool:MaxReconnectionAttempts": "Max Reconnection Attempts", "McpAITool:MaxReconnectionAttemptsDesc": "The maximum number of reconnection attempts. The default is 5.", - "McpAITool:CurrentAccessToken": "Use the current user token" + "McpAITool:CurrentAccessToken": "Use the current user token", + "McpAITool:UseOAuth": "OAuth Authorization", + "McpAITool:RedirectUri": "Redirect Uri", + "McpAITool:ClientId": "Client Id", + "McpAITool:ClientSecret": "Client Secret", + "McpAITool:ClientMetadataDocumentUri": "Client Metadata Uri", + "McpAITool:Scopes": "Scopes", + "McpAITool:ScopesDesc": "Multiple authorization scopes are separated by spaces.", + "McpAITool:AdditionalParameters": "Additional Parameters" } } \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/Localization/Resources/zh-Hans.json b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/Localization/Resources/zh-Hans.json index 401f0ad7b..4d5ceee7a 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/Localization/Resources/zh-Hans.json +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/Localization/Resources/zh-Hans.json @@ -9,6 +9,14 @@ "McpAITool:ConnectionTimeoutDesc": "用于建立与 SSE 服务器初始连接的超时时间, 默认: 30秒.", "McpAITool:MaxReconnectionAttempts": "最大重连次数", "McpAITool:MaxReconnectionAttemptsDesc": "最大重连次数, 默认: 5次.", - "McpAITool:CurrentAccessToken": "使用当前用户Token" + "McpAITool:CurrentAccessToken": "使用当前用户Token", + "McpAITool:UseOAuth": "使用OAuth认证", + "McpAITool:RedirectUri": "重定向Uri", + "McpAITool:ClientId": "客户端Id", + "McpAITool:ClientSecret": "客户端密钥", + "McpAITool:ClientMetadataDocumentUri": "发现文档Uri", + "McpAITool:Scopes": "授权范围", + "McpAITool:ScopesDesc": "多个授权范围以空格分隔.", + "McpAITool:AdditionalParameters": "附加参数" } } \ No newline at end of file diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/McpAIToolDefinitionExtenssions.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/McpAIToolDefinitionExtenssions.cs index 9914af69c..ee1cc5af2 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/McpAIToolDefinitionExtenssions.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/McpAIToolDefinitionExtenssions.cs @@ -1,4 +1,5 @@ -using ModelContextProtocol.Client; +using ModelContextProtocol.Authentication; +using ModelContextProtocol.Client; using System; using System.Collections.Generic; using Volo.Abp; @@ -147,4 +148,88 @@ public static class McpAIToolDefinitionExtenssions return new Dictionary(); } + + #region OAuth + + public const string UseOAuth = "McpUseOAuth"; + public const string RedirectUri = "McpRedirectUri"; + public const string ClientId = "McpClientId"; + public const string ClientSecret = "McpClientSecret"; + public const string ClientMetadataDocumentUri = "McpClientMetadataDocumentUri"; + public const string Scopes = "McpScopes"; + public const string AuthorizationParameters = "McpAuthorizationParameters"; + + public static AIToolDefinition WithMcpOAuth(this AIToolDefinition definition, ClientOAuthOptions oAuthOptions) + { + Check.NotNull(oAuthOptions, nameof(oAuthOptions)); + + definition.WithProperty(UseOAuth, true); + definition.WithProperty(RedirectUri, oAuthOptions.RedirectUri.ToString()); + if (!oAuthOptions.ClientId.IsNullOrWhiteSpace()) + { + definition.WithProperty(ClientId, oAuthOptions.ClientId); + } + if (!oAuthOptions.ClientSecret.IsNullOrWhiteSpace()) + { + definition.WithProperty(ClientSecret, oAuthOptions.ClientSecret); + } + if (oAuthOptions.ClientMetadataDocumentUri != null) + { + definition.WithProperty(ClientMetadataDocumentUri, oAuthOptions.ClientMetadataDocumentUri.ToString()); + } + if (oAuthOptions.Scopes != null) + { + definition.WithProperty(Scopes, oAuthOptions.Scopes.JoinAsString(" ")); + } + if (oAuthOptions.AdditionalAuthorizationParameters != null) + { + definition.WithProperty(AuthorizationParameters, oAuthOptions.AdditionalAuthorizationParameters); + } + + return definition; + } + + public static ClientOAuthOptions? GetMcpOAuth(this AIToolDefinition definition) + { + if (definition.Properties.TryGetValue(UseOAuth, out var useOAuthObj) && useOAuthObj is true && + definition.Properties.TryGetValue(RedirectUri, out var redirectUriObj) && redirectUriObj != null) + { + var oAuthOptions = new ClientOAuthOptions + { + RedirectUri = new Uri(redirectUriObj.ToString()!) + }; + if (definition.Properties.TryGetValue(ClientId, out var clientIdObj) && clientIdObj != null) + { + oAuthOptions.ClientId = clientIdObj.ToString(); + } + if (definition.Properties.TryGetValue(ClientSecret, out var clientSecretObj) && clientSecretObj != null) + { + oAuthOptions.ClientSecret = clientSecretObj.ToString(); + } + if (definition.Properties.TryGetValue(ClientMetadataDocumentUri, out var clientMetadataDocumentUriObj) && clientMetadataDocumentUriObj != null) + { + var clientMetadataDocumentUri = clientMetadataDocumentUriObj.ToString(); + if (!clientMetadataDocumentUri.IsNullOrWhiteSpace()) + { + oAuthOptions.ClientMetadataDocumentUri = new Uri(clientMetadataDocumentUri); + } + } + if (definition.Properties.TryGetValue(Scopes, out var scopesObj) && scopesObj != null) + { + oAuthOptions.Scopes = scopesObj.ToString()?.Split(" "); + } + if (definition.Properties.TryGetValue(AuthorizationParameters, out var parametersObj) && parametersObj != null) + { + if (parametersObj is IDictionary parameters) + { + oAuthOptions.AdditionalAuthorizationParameters = parameters; + } + } + return oAuthOptions; + } + + return null; + } + + #endregion } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/McpAIToolProvider.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/McpAIToolProvider.cs index e9ba2bd10..b78a9ab87 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/McpAIToolProvider.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/McpAIToolProvider.cs @@ -2,12 +2,15 @@ using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using ModelContextProtocol.Authentication; using ModelContextProtocol.Client; using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; +using System.Web; using Volo.Abp.DependencyInjection; using Volo.Abp.Http.Client.Authentication; using Volo.Abp.Localization; @@ -57,7 +60,37 @@ public class McpAIToolProvider : IAIToolProvider, ITransientDependency LocalizableString.Create("McpAITool:MaxReconnectionAttemptsDesc")), AIToolPropertyDescriptor.CreateBoolProperty( McpAIToolDefinitionExtenssions.CurrentAccessToken, - LocalizableString.Create("McpAITool:CurrentAccessToken"))]; + LocalizableString.Create("McpAITool:CurrentAccessToken")), + + AIToolPropertyDescriptor.CreateBoolProperty( + McpAIToolDefinitionExtenssions.UseOAuth, + LocalizableString.Create("McpAITool:UseOAuth")), + AIToolPropertyDescriptor.CreateStringProperty( + McpAIToolDefinitionExtenssions.RedirectUri, + LocalizableString.Create("McpAITool:RedirectUri"), + required: true) + .DependsOn(McpAIToolDefinitionExtenssions.UseOAuth, true), + AIToolPropertyDescriptor.CreateStringProperty( + McpAIToolDefinitionExtenssions.ClientId, + LocalizableString.Create("McpAITool:ClientId")) + .DependsOn(McpAIToolDefinitionExtenssions.UseOAuth, true), + AIToolPropertyDescriptor.CreateStringProperty( + McpAIToolDefinitionExtenssions.ClientSecret, + LocalizableString.Create("McpAITool:ClientSecret")) + .DependsOn(McpAIToolDefinitionExtenssions.UseOAuth, true), + AIToolPropertyDescriptor.CreateStringProperty( + McpAIToolDefinitionExtenssions.ClientMetadataDocumentUri, + LocalizableString.Create("McpAITool:ClientMetadataDocumentUri")) + .DependsOn(McpAIToolDefinitionExtenssions.UseOAuth, true), + AIToolPropertyDescriptor.CreateStringProperty( + McpAIToolDefinitionExtenssions.Scopes, + LocalizableString.Create("McpAITool:Scopes"), + LocalizableString.Create("McpAITool:ScopesDesc")) + .DependsOn(McpAIToolDefinitionExtenssions.UseOAuth, true), + AIToolPropertyDescriptor.CreateDictionaryProperty( + McpAIToolDefinitionExtenssions.AuthorizationParameters, + LocalizableString.Create("McpAITool:AdditionalParameters")) + .DependsOn(McpAIToolDefinitionExtenssions.UseOAuth, true)]; } public async virtual Task CreateToolsAsync(AIToolDefinition definition) @@ -80,14 +113,41 @@ public class McpAIToolProvider : IAIToolProvider, ITransientDependency httpClientTransportOptions.AdditionalHeaders.TryAdd(header.Key, header.Value); } - if (definition.IsUseMcpCurrentAccessToken()) + var oAuthOptions = definition.GetMcpOAuth(); + if (oAuthOptions != null) + { + httpClientTransportOptions.OAuth = new ClientOAuthOptions + { + RedirectUri = oAuthOptions.RedirectUri, + ClientId = oAuthOptions.ClientId, + ClientSecret = oAuthOptions.ClientSecret, + ClientMetadataDocumentUri = oAuthOptions.ClientMetadataDocumentUri, + Scopes = oAuthOptions.Scopes, + AdditionalAuthorizationParameters = oAuthOptions.AdditionalAuthorizationParameters, + }; + var authCodeProvider = ServiceProvider.GetService(); + if (authCodeProvider != null) + { + httpClientTransportOptions.OAuth.AuthorizationRedirectDelegate = async (Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken) => + { + var authCodeHandleContext = new McpAuthorizationCodeHandleContext( + ServiceProvider, + httpClient, + authorizationUrl, + redirectUri, + cancellationToken); + + return await authCodeProvider.HandleAsync(authCodeHandleContext); + }; + } + } + else if (definition.IsUseMcpCurrentAccessToken()) { var accessTokenProvider = ServiceProvider.GetRequiredService(); var token = await accessTokenProvider.GetTokenAsync(); if (!token.IsNullOrWhiteSpace()) { - // TODO: 使用OAuth配置? httpClientTransportOptions.AdditionalHeaders.TryAdd("Authorization", $"Bearer {token}"); } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/McpAuthorizationCodeHandleContext.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/McpAuthorizationCodeHandleContext.cs new file mode 100644 index 000000000..4dee44868 --- /dev/null +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/LINGYUN/Abp/AI/Tools/Mcp/McpAuthorizationCodeHandleContext.cs @@ -0,0 +1,26 @@ +using System; +using System.Net.Http; +using System.Threading; + +namespace LINGYUN.Abp.AI.Tools.Mcp; +public class McpAuthorizationCodeHandleContext +{ + public IServiceProvider ServiceProvider { get; } + public HttpClient HttpClient { get; } + public Uri AuthorizationUrl { get; } + public Uri RedirectUri { get; } + public CancellationToken CancellationToken { get; } + public McpAuthorizationCodeHandleContext( + IServiceProvider serviceProvider, + HttpClient httpClient, + Uri authorizationUrl, + Uri redirectUri, + CancellationToken cancellationToken) + { + ServiceProvider = serviceProvider; + HttpClient = httpClient; + AuthorizationUrl = authorizationUrl; + RedirectUri = redirectUri; + CancellationToken = cancellationToken; + } +} diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/Microsoft/Extensions/DependencyInjection/HttpClientMcpAIToolExtenssions.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/Microsoft/Extensions/DependencyInjection/HttpClientMcpAIToolExtenssions.cs index 98d5bc1c8..b28581d44 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/Microsoft/Extensions/DependencyInjection/HttpClientMcpAIToolExtenssions.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools.Mcp/Microsoft/Extensions/DependencyInjection/HttpClientMcpAIToolExtenssions.cs @@ -6,7 +6,14 @@ internal static class HttpClientMcpAIToolExtenssions private const string McpAIToolClient = "__AbpAIMcpToolClient"; public static IServiceCollection AddMcpAIToolClient(this IServiceCollection services) { - services.AddHttpClient(McpAIToolClient); + services.AddHttpClient(McpAIToolClient) + .ConfigurePrimaryHttpMessageHandler(() => + { + return new HttpClientHandler() + { + AllowAutoRedirect = false, + }; + }); return services; } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools/LINGYUN/Abp/AI/Tools/AIToolPropertyDescriptor.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools/LINGYUN/Abp/AI/Tools/AIToolPropertyDescriptor.cs index 9d0194509..0ddc9ec51 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools/LINGYUN/Abp/AI/Tools/AIToolPropertyDescriptor.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AI.Tools/LINGYUN/Abp/AI/Tools/AIToolPropertyDescriptor.cs @@ -21,6 +21,7 @@ public class AIToolPropertyDescriptor public List> Options { get; } public ILocalizableString DisplayName { get; } public ILocalizableString? Description { get; } + public List> Dependencies { get; } private AIToolPropertyDescriptor( string name, PropertyValueType valueType, @@ -35,6 +36,7 @@ public class AIToolPropertyDescriptor Required = required; Options = new List>(); + Dependencies = new List>(); } public static AIToolPropertyDescriptor CreateStringProperty( @@ -113,8 +115,17 @@ public class AIToolPropertyDescriptor return propertyDescriptor; } - public void WithOption(string name, object value) + public AIToolPropertyDescriptor WithOption(string name, object value) { Options.Add(new NameValue(name, value)); + + return this; + } + + public AIToolPropertyDescriptor DependsOn(string name, object value) + { + Dependencies.Add(new NameValue(name, value)); + + return this; } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Tools/Dtos/AIToolPropertyDescriptorDto.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Tools/Dtos/AIToolPropertyDescriptorDto.cs index 4874482c3..9a83898c2 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Tools/Dtos/AIToolPropertyDescriptorDto.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application.Contracts/LINGYUN/Abp/AIManagement/Tools/Dtos/AIToolPropertyDescriptorDto.cs @@ -10,4 +10,5 @@ public class AIToolPropertyDescriptorDto public List> Options { get; set; } public string DisplayName { get; set; } public string? Description { get; set; } + public List> Dependencies { get; set; } } diff --git a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Tools/AIToolDefinitionAppService.cs b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Tools/AIToolDefinitionAppService.cs index c4ebf5987..7b4265a97 100644 --- a/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Tools/AIToolDefinitionAppService.cs +++ b/aspnet-core/modules/ai/LINGYUN.Abp.AIManagement.Application/LINGYUN/Abp/AIManagement/Tools/AIToolDefinitionAppService.cs @@ -66,6 +66,7 @@ public class AIToolDefinitionAppService : Required = prop.Required, ValueType = prop.ValueType.ToString(), DisplayName = prop.DisplayName.Localize(StringLocalizerFactory), + Dependencies = prop.Dependencies, }; if (prop.Description != null) {