Hi,
ForwardedHeadersOptions only trusts loopback proxies by default. Your reverse proxy connects from a Docker network IP, so the X-Forwarded-Proto header is ignored and OpenIddict still sees an HTTP request.
Please clear the known proxy lists:
Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownIPNetworks.Clear();
options.KnownProxies.Clear();
});
Also make sure the proxy sends X-Forwarded-Proto: https. If the app can be reached without going through the proxy, add the proxy IP to KnownProxies instead of clearing the lists.
Thanks
Hi,
Use jQuery 4 + Migrate 4 and keep the ABP-managed plugins unchanged. This is the default setup of ABP 10.7.0 / LeptonX 5.7.0 MVC.
The 10.6 project template had a jquery: 3.7.1 resolution, and the 10.7 template changed it to 4.0.0. Please update it in your host package.json:
"resolutions": {
"jquery": "4.0.0"
}
Then run yarn and abp install-libs in the host project, and rebuild/redeploy it. Your yarn.lock can stay, Yarn updates it.
The declared jQuery ranges of bootstrap-datepicker, timeago and jquery-validation-unobtrusive don't include 4. That's expected in 10.7. timeago and jquery-validation-unobtrusive still call APIs that jQuery 4 removed ($.isFunction, $.trim, $.parseJSON), and jQuery Migrate 4 is in the default bundle to bring them back. Please keep Migrate and don't override these packages. Yarn will still print incompatible warnings for these ranges during install, you can ignore them.
There is no jQuery 3.7.1 setup for 10.7. The default bundle includes Migrate 4, and Migrate 4 requires jQuery 4.
In 10.8 we replace or patch these packages, so they no longer need Migrate:
timeago is replaced with timeago.js: https://github.com/abpframework/abp/issues/25965bootstrap-datepicker copy that works with jQuery 4: https://github.com/abpframework/abp/issues/25970jquery-validation-unobtrusive copy that works with jQuery 4: https://github.com/abpframework/abp/issues/25972For the browser checks:
jQuery.fn.jquery and jQuery.migrateVersion in the browser console. They should return 4.0.0 and 4.0.2, and JQMIGRATE: jQuery 4.x REQUIRED should be gone.bootstrap-datepicker: ABP applies it to <input type="date"> and input.datepicker fields. Open one of your forms with such a field, pick a date, clear it, submit and reopen it.timeago: Audit Logs, open a log that has entity changes, and the entity change history. The relative times should render.Other JQMIGRATE: messages like jQuery.trim() is removed or jQuery.proxy() is deprecated are expected while Migrate is in the bundle. They show where Migrate is filling in an API. Look for JS errors and broken behavior on these pages.
Thanks
Hi,
The test host licensing part is done then. I'll mark this question as resolved.
Thanks
Hi,
All four are bugs in the AI Management module. The MCP sessions are cached for the whole process and nothing refreshes or removes them.
You can replace McpToolProvider in your Domain project for now. It creates a new session when the MCP server configuration changes. When a tool call gets 404, it starts a new session and retries the call once.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.AIManagement.Mcp;
namespace YourProjectName;
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IMcpToolProvider), typeof(McpToolProvider))]
public class MyMcpToolProvider : McpToolProvider
{
public MyMcpToolProvider(
ILogger<McpToolProvider> logger,
IMcpServerConfigurationStore mcpServerConfigurationStore,
IOptions<McpClientFactoryOptions> options,
IServiceProvider serviceProvider,
IMcpToolCatalogCache toolCatalogCache)
: base(logger, mcpServerConfigurationStore, options, serviceProvider, toolCatalogCache)
{
}
protected override async Task<IReadOnlyList<AIFunction>> GetToolsForConfigurationAsync(
McpClientCreationConfiguration configuration,
string workspaceName,
CancellationToken cancellationToken)
{
IMcpClientSession? session = null;
try
{
session = await GetOrCreateSessionAsync(configuration, cancellationToken);
var tools = await session.GetToolsAsync(cancellationToken);
await ToolCatalogCache.SetAsync(
configuration.McpServerId,
McpToolCatalogFingerprint.Compute(configuration),
McpToolCatalogMappingHelper.FromAIFunctions(tools),
cancellationToken: cancellationToken);
return tools
.Select(tool => (AIFunction)new SessionRecoveringMcpTool(tool, this, configuration))
.ToList();
}
catch (Exception ex)
{
if (session != null)
{
await RemoveSessionAsync(configuration, session);
}
Logger.LogWarning(ex, "Failed to get tools from MCP server '{ServerName}' for workspace '{WorkspaceName}'",
configuration.Name, workspaceName);
return Array.Empty<AIFunction>();
}
}
protected override async Task<IMcpClientSession> GetOrCreateSessionAsync(
McpClientCreationConfiguration configuration,
CancellationToken cancellationToken)
{
// The key contains the full configuration, so editing an MCP server creates a new session.
var sessionKey = GetSessionKey(configuration);
if (ActiveSessions.TryGetValue(sessionKey, out var existingSession) && existingSession.IsConnected)
{
return existingSession;
}
var gate = SessionCreateLocks.GetOrAdd(sessionKey, _ => new SemaphoreSlim(1, 1));
await gate.WaitAsync(cancellationToken);
try
{
if (ActiveSessions.TryGetValue(sessionKey, out existingSession) && existingSession.IsConnected)
{
return existingSession;
}
if (existingSession != null)
{
ActiveSessions.TryRemove(sessionKey, out _);
await DisposeSessionAsync(existingSession);
}
var session = await CreateSessionAsync(configuration, cancellationToken);
ActiveSessions[sessionKey] = session;
return session;
}
finally
{
gate.Release();
}
}
public virtual async Task<object?> InvokeToolAsync(
McpClientCreationConfiguration configuration,
string toolName,
AIFunctionArguments arguments,
CancellationToken cancellationToken)
{
var session = await GetOrCreateSessionAsync(configuration, cancellationToken);
try
{
return await InvokeToolAsync(session, toolName, arguments, cancellationToken);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
// The MCP server dropped the session (idle timeout or restart), start a new one and retry once.
await RemoveSessionAsync(configuration, session);
session = await GetOrCreateSessionAsync(configuration, cancellationToken);
return await InvokeToolAsync(session, toolName, arguments, cancellationToken);
}
}
protected virtual async Task<object?> InvokeToolAsync(
IMcpClientSession session,
string toolName,
AIFunctionArguments arguments,
CancellationToken cancellationToken)
{
var tools = await session.GetToolsAsync(cancellationToken);
var tool = tools.FirstOrDefault(t => t.Name == toolName)
?? throw new InvalidOperationException($"The MCP tool '{toolName}' is not available anymore.");
return await tool.InvokeAsync(arguments, cancellationToken);
}
protected virtual async Task RemoveSessionAsync(McpClientCreationConfiguration configuration, IMcpClientSession session)
{
// Only remove the given session, another request may have replaced it already.
if (ActiveSessions.TryRemove(new KeyValuePair<string, IMcpClientSession>(GetSessionKey(configuration), session)))
{
await DisposeSessionAsync(session);
}
}
protected virtual string GetSessionKey(McpClientCreationConfiguration configuration)
{
var json = JsonSerializer.Serialize(new
{
Fingerprint = McpToolCatalogFingerprint.Compute(configuration),
configuration.Name,
configuration.ApiKey,
configuration.CustomAuthHeaderValue
});
return $"{configuration.McpServerId}_{Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(json)))}";
}
protected virtual async Task DisposeSessionAsync(IMcpClientSession session)
{
try
{
await session.DisposeAsync();
}
catch (Exception ex)
{
Logger.LogDebug(ex, "Failed to dispose MCP session '{SessionId}'", session.SessionId);
}
}
}
public class SessionRecoveringMcpTool : DelegatingAIFunction
{
protected MyMcpToolProvider Provider { get; }
protected McpClientCreationConfiguration Configuration { get; }
public SessionRecoveringMcpTool(
AIFunction innerFunction,
MyMcpToolProvider provider,
McpClientCreationConfiguration configuration)
: base(innerFunction)
{
Provider = provider;
Configuration = configuration;
}
protected override async ValueTask<object?> InvokeCoreAsync(
AIFunctionArguments arguments,
CancellationToken cancellationToken)
{
return await Provider.InvokeToolAsync(Configuration, Name, arguments, cancellationToken);
}
}
Then close the sessions on shutdown in your Domain module:
using System.Threading.Tasks;
using Volo.Abp;
using Volo.AIManagement.Mcp;
public override async Task OnApplicationShutdownAsync(ApplicationShutdownContext context)
{
await McpToolProvider.DisposeAllSessionsAsync();
}
This doesn't refresh the tool list of a running session. The updated tool list is used in the next chat request after a new session is created, for example after a 404 or a configuration change. Sessions of an old configuration are closed on shutdown.
We will fix all four in the module, including handling notifications/tools/list_changed. Your ticket has been refunded.
Thanks
Hi,
With Logout from all devices enabled, older sessions are removed when that user logs in again.
Your screenshot still shows multiple sessions for a host user (TenantId is NULL). For host users, save the setting as the host admin, then log in again with that host user.
Thanks
Hi,
Saving the setting doesn't remove the existing sessions. It's applied the next time the user logs in, and then the other sessions of that user are deleted.
Please save Logout from all devices again (as the host admin for a host user, or as that tenant's admin for a tenant user). Then log in with the same user in two browsers, one after the other, and check that user's rows in the AbpSessions table.
If the rows from the first browser are still there, please share the result of this query:
SELECT Name, Value, ProviderName, ProviderKey
FROM AbpSettings
WHERE Name = 'Abp.Identity.Session.PreventConcurrentLogin'
Thanks
Hi,
The template's Dockerfile.local copies openiddict.pfx into the image as root, but the app runs as a non-root user and can't read it.
Use this Nicma.Idp/Dockerfile.local:
FROM mcr.microsoft.com/dotnet/sdk:10.0
USER $APP_UID
EXPOSE 8080
EXPOSE 8081
COPY --chown=$APP_UID bin/Release/net10.0/publish/ app/
WORKDIR /app
ENTRYPOINT ["dotnet", "Nicma.Idp.dll"]
Then build the images again with etc/build/build-images-locally.ps1 and run etc/docker/run-docker.ps1.
Your attached file uses USER root. That also works, but only after the image is rebuilt, and it's better to keep the non-root user.
If the error is still there after the rebuild, please share the output of these commands:
docker inspect idp-api-container --format 'User={{.Config.User}} Image={{.Image}}'
docker image inspect nicma/idp-api:latest --format 'User={{.Config.User}} Id={{.Id}}'
We will fix this in the template. Your ticket has been refunded.
Thanks
Great 👍
Hi,
Your code still calls base.OnPostAsync(action), so the standard sign-in is still used. Note that this setting doesn't block the second login. It logs out the older sessions.
I need more details to see where it goes wrong.
Prevent concurrent login setting, and whether you saved it on the host side or in that tenant.AbpSessions table before and after the second login.IsDynamicClaimsEnabled configuration and the middleware order (UseAuthentication, UseDynamicClaims, UseAuthorization) in your AuthServer and HttpApi.Host modules.CustomLoginModel? Did you change anything else in the Account module?Please also enable Debug logs on the AuthServer and the HttpApi.Host, and share the logs that cover both logins and the request from the first browser. Remove any passwords and tokens before sending.
https://abp.io/support/questions/8622/How-to-enable-Debug-logs-for-troubleshoot-problems
You can send the logs to liming.ma@volosoft.com
Thanks
Hi,
The System option is not in LPX_THEMES. The theme service adds it on its own, so filtering the list doesn't remove it. You can turn it off with disableSystemOption:
ThemeLeptonXModule.forRoot({
defaultTheme: 'light',
themeOptions: {
localStorageKey: 'LPX_THEME',
disableSystemOption: true,
},
}),
Keep your LPX_THEMES provider as it is. localStorageKey is required when you pass themeOptions, and LPX_THEME is its default value.
Thanks