Activities of "maliming"

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/25965
  • ABP ships its own bootstrap-datepicker copy that works with jQuery 4: https://github.com/abpframework/abp/issues/25970
  • ABP ships its own jquery-validation-unobtrusive copy that works with jQuery 4: https://github.com/abpframework/abp/issues/25972

For the browser checks:

  1. First run 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.
  2. Validation: login, register, and a create/edit modal with required fields. Submit with empty fields and check the validation messages, then submit valid data.
  3. 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.
  4. timeago: Audit Logs, open a log that has entity changes, and the entity change history. The relative times should render.
  5. Date range pickers (a different plugin, but also jQuery based): Identity > Users filters, the lock user modal, and the security logs filters.

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.

  1. The exact ABP version (10.0.x, 10.1.x ...).
  2. The steps to reproduce it with the same user and tenant in two separate browsers. After the second login, call any API from the first browser and share the HTTP status and response.
  3. The value of the Prevent concurrent login setting, and whether you saved it on the host side or in that tenant.
  4. That user's rows in the AbpSessions table before and after the second login.
  5. The IsDynamicClaimsEnabled configuration and the middleware order (UseAuthentication, UseDynamicClaims, UseAuthorization) in your AuthServer and HttpApi.Host modules.
  6. Does it work again if you temporarily remove 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

Hi,

With that setup only the offline check runs, so the license code is all the test host needs.

I'll leave this question open. Post the result here when you have it.

Thanks

Showing 51 to 60 of 12919 entries
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on October 30, 2026, 06:34
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.