MentorAgent Docs
Menu

How-to guidePackages: Novior.MentorAgent, Novior.MentorAgent.Server, Novior.MentorAgent.Blazor

Blazor WebAssembly and Auto

Keep the widget on the server, or run it in a WebAssembly or Auto client with Novior.MentorAgent.Blazor, backed by Novior.MentorAgent.Server.

View as Markdown
On this page

This page is for a standalone Blazor WebAssembly app, and for a Blazor Web App with Auto or WebAssembly interactivity. In such an app the agent always runs on the server. The widget can stay on the server too (option A), or run in the browser and reach the agent over SignalR (option B).

Choose it for a Blazor Web App with Auto or WebAssembly interactivity whose layout is rendered statically: Interactivity location: Per page/component in the template. Only the widget is pinned to Server interactivity, that is, it gets its own InteractiveServer render mode. The rest of the app keeps its render mode. You need neither Novior.MentorAgent.Server nor Novior.MentorAgent.Blazor. An app created with WebAssembly interactivity also needs Server interactivity turned on.

Choose it for a standalone Blazor WebAssembly app, for a Blazor Web App with Interactivity location: Global, or when the widget must run in the browser. The server project gets Novior.MentorAgent.Server, which adds the SignalR hub; the client project gets Novior.MentorAgent.Blazor, which gives the same <ChatWidget /> a hub connection. The model key never reaches the browser, and the agent's features (specialists, RAG, MCP, A2A, memory, confirmation) all run on the server.

For a desktop app see .NET MAUI; for a JavaScript or mobile client, Any front end.

Option A. The server project needs Novior.MentorAgent and the package of your model provider, as on Blazor Server.

Option B. In the server project, Novior.MentorAgent.Server brings Novior.MentorAgent with it:

.NET CLI

Terminal
dotnet add package Novior.MentorAgent.Server

PackageReference

YourApp.csproj
<ItemGroup>
  <PackageReference Include="Novior.MentorAgent.Server" Version="1.0.0" />
</ItemGroup>

Central Package Management

Directory.Packages.props
<ItemGroup>
  <PackageVersion Include="Novior.MentorAgent.Server" Version="1.0.0" />
</ItemGroup>
YourApp.csproj
<ItemGroup>
  <PackageReference Include="Novior.MentorAgent.Server" />
</ItemGroup>

File-based app

app.cs
#:package Novior.MentorAgent.Server@1.0.0

With either option, MentorAgent does not include a model provider package. The server code on this page uses OpenAI, so add its package to the server project:

Terminal
dotnet add package Microsoft.Extensions.AI.OpenAI --version 10.6.0

For another provider, see Model providers.

In the client project:

.NET CLI

Terminal
dotnet add package Novior.MentorAgent.Blazor

PackageReference

YourApp.csproj
<ItemGroup>
  <PackageReference Include="Novior.MentorAgent.Blazor" Version="1.0.0" />
</ItemGroup>

Central Package Management

Directory.Packages.props
<ItemGroup>
  <PackageVersion Include="Novior.MentorAgent.Blazor" Version="1.0.0" />
</ItemGroup>
YourApp.csproj
<ItemGroup>
  <PackageReference Include="Novior.MentorAgent.Blazor" />
</ItemGroup>

File-based app

app.cs
#:package Novior.MentorAgent.Blazor@1.0.0

Option A. Register MentorAgent in the server project with the same Program.cs lines as on Blazor Server:

Program.cs (server)
using Novior.MentorAgent.Abstractions.Models;   // MentorLanguage, MentorTheme, ChatPosition, MentorshipLevel
using Novior.MentorAgent.Extensions;            // AddMentorAgent
using Microsoft.Extensions.AI;           // AsIChatClient
using OpenAI;                            // OpenAIClient
Program.cs (server)
builder.Services.AddMentorAgent(options =>
{
    options.AppName = "Case management";
    options.ChatClient = new OpenAIClient(builder.Configuration["OpenAI:Key"]
            ?? throw new InvalidOperationException("Set OpenAI:Key with dotnet user-secrets."))
        .GetChatClient("gpt-4o-mini")
        .AsIChatClient();
    options.ScanAssemblies = [typeof(Program).Assembly];
});

The Auto template already turns on Server interactivity. The WebAssembly template does not: in the same Program.cs, chain .AddInteractiveServerComponents() to AddRazorComponents(), and .AddInteractiveServerRenderMode() to MapRazorComponents<App>(). Without them every page answers HTTP 500.

Then pin the widget to Server interactivity in the server project's layout:

Components/Layout/MainLayout.razor
@using Novior.MentorAgent.Abstractions.Components
<ChatWidget @rendermode="InteractiveServer" />

This works only because the layout is static. With Interactivity location: Global (--all-interactive), option A fails:

  • the layout is in the client project, inside the interactive router, and a child component cannot switch to another interactive render mode;
  • the client project does not know ChatWidget without a MentorAgent package, and the build stops. With the package added, the build, the startup and the prerendered page all succeed: the error appears only in the browser, when WebAssembly renders the layout;
  • with Auto, the widget works while the app still runs on the server, and fails once it runs in WebAssembly.

With global interactivity, use option B.

Option B.

  1. In the server project, register the services and map the hub:

    Program.cs (server)
    using Novior.MentorAgent.Extensions;          // AddMentorAgent
    using Novior.MentorAgent.Server.Extensions;   // AddMentorAgentServer, MapMentorAgentServer
    using Microsoft.Extensions.AI;         // AsIChatClient
    using OpenAI;                          // OpenAIClient
    Program.cs (server)
    builder.Services.AddMentorAgent(options =>
    {
        options.AppName = "Case management";
        options.ChatClient = new OpenAIClient(builder.Configuration["OpenAI:Key"]
                ?? throw new InvalidOperationException("Set OpenAI:Key with dotnet user-secrets."))
            .GetChatClient("gpt-4o-mini")
            .AsIChatClient();
        options.ScanAssemblies = [typeof(Program).Assembly];
    });
    builder.Services.AddMentorAgentServer();
    // after var app = builder.Build();
    app.MapMentorAgentServer();   // /mentor-hub, /mentor/chat, /mentor/approve, /mentor/cancel ...
  2. In the client project, register the widget's services:

    Program.cs (client)
    using Novior.MentorAgent.Abstractions.Models;   // MentorLanguage, MentorTheme
    using Novior.MentorAgent.Blazor.Extensions;     // AddMentorAgentBlazor
    Program.cs (client)
    // The widget posts Stop and confirmations through an HttpClient: the standalone template registers this line, a Blazor Web App client does not
    builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
    
    builder.Services.AddMentorAgentBlazor(options =>
    {
        // Same origin as the server: the default "/mentor-hub" is resolved against the base address of the page
        options.HubUrl = "/mentor-hub";
    });

    HubUrl defaults to /mentor-hub. A relative value is resolved against the base address of the page, as a browser does: /mentor-hub points to the root of the origin, and mentor-hub to the base path of the app. It works when the server also serves the client, as in a Blazor Web App or a hosted WebAssembly app. For a server on another origin, set an absolute HubUrl: see Complete configuration.

  3. Add the widget to a layout or page of the client project, as on Blazor Server. In a Blazor Web App the client project has a layout only with Interactivity location: Global:

    Layout/MainLayout.razor (client)
    @using Novior.MentorAgent.Abstractions.Components
    <ChatWidget />
  4. In a standalone app, reference the widget's stylesheet and script in wwwroot/index.html. That page is served before .NET starts in the browser, so the widget cannot add them in time:

    HTML
    <link href="_content/Novior.MentorAgent.Abstractions/css/MentorAgent.css?v=1.0.0" rel="stylesheet" />
    <script src="_content/Novior.MentorAgent.Abstractions/js/MentorAgent.js?v=1.0.0"></script>

    Keep the ?v= query string and change it on every upgrade: nothing else tells the browser that its cached copy is old.

    A Blazor Web App has no index.html. Its server prerenders the widget, and the widget adds both tags to the <head> of the page itself, as on Blazor Server. Add the two lines to Components/App.razor only if you turn prerendering off.

A standalone client on another origin, with signed-in users. On the server, allow the client's origin and require authentication on the hub:

Program.cs (server)
builder.Services.AddCors(options => options.AddPolicy("MentorClients", policy => policy
    .WithOrigins("https://localhost:7001")   // the WebAssembly app's origin
    .AllowAnyHeader()
    .AllowAnyMethod()
    .AllowCredentials()));                   // SignalR needs credentials

// Plus your authentication scheme (JWT bearer, cookies): builder.Services.AddAuthentication()...
app.UseCors("MentorClients");   // before authentication and authorization
app.UseAuthentication();
app.UseAuthorization();
app.MapMentorAgentServer(configureTransports: e => e.RequireAuthorization());

On the client, point to the server and send the user's token:

Program.cs (client)
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

builder.Services.AddMentorAgentBlazor(options =>
{
    // Server on another origin: absolute URL, and CORS on the server
    options.HubUrl = "https://api.example.com/mentor-hub";
    // The signed-in user's token, for the hub, Stop and confirmations
    options.AccessTokenProvider = () => Task.FromResult(tokenStore.Token);

    // The widget: on WebAssembly these settings live here, not in MentorOptions
    options.BotName = "Case assistant";
    options.Language = MentorLanguage.English;
    options.Theme = MentorTheme.Minimal;
    options.EnableSuggestions = true;
});

TokenStore stands for whatever your application uses to keep the signed-in user's token.

OptionProjectWhy it matters
HubUrlclientThe default /mentor-hub is resolved against the base address of the page. Set an absolute URL for a server on another origin. The origin of an absolute URL is also where the widget posts Stop and confirmations
AccessTokenProviderclientThe user's token, read on connect and on every reconnect, and sent with Stop and confirmations. Without it every user is anonymous to the server
ConfigureConnectionclientAnything else the hub connection needs: a header, a transport, cookies
BotName, Theme, PrimaryColor, Position, WelcomeMessage, LanguageclientOn WebAssembly the widget reads its look and its own texts from the client options, not from MentorOptions. In a Blazor Web App the server prerenders it with MentorOptions first: give both the same values
EnableSuggestionsclientOff by default on the client, while it is on by default in MentorOptions
EnableOnboardingTourclientTurn it on in the client options. The server answers GET /mentor/tour whatever its own value. In a Blazor Web App, give MentorOptions the same value, because the server prerenders the widget
EnableImageInputbothTurn it on in the client options and in MentorOptions
Everything about the agentserverModel, agents, RAG, memory, MCP, A2A, costs and limits stay in MentorOptions, on the server. Language there sets the language of the answers

Every server option is in Configuration options, and every client option under MentorAgentBlazorOptions.

  • Who the user is. AddMentorAgentServer() passes the user of the hub connection to MentorAgent. RequiredRoles, per-user memory and the rate limit then work as on Blazor Server. The client must send the token through AccessTokenProvider.
  • In a Blazor Web App the hub still knows the user. AddRazorComponents() and AddMentorAgentServer() can come in either order. MentorAgent reads the user of the hub connection from the request, before it asks Blazor's own provider, which works only inside a circuit. So role-gated actions, per-user memory and the rate limit work with option B, if the client sends the token through AccessTokenProvider. To add claims or roles for the hub, see Secure the endpoints.
  • With Auto, the user comes from two places. On the first visit the page runs on the server, and the widget runs in the circuit: the user is the one of the circuit. After the switch to WebAssembly the widget uses the hub: the user is the one of the token that AccessTokenProvider sends. Without a token, the widget is anonymous after the switch and role-gated actions are refused.
  • When the token is read. The widget opens the connection with the first message and keeps it for the life of the page. A user who signs in after that stays anonymous until the page reloads.
  • JWT bearer. In the browser, WebSocket and Server-Sent Events connections cannot carry an Authorization header, so SignalR sends the token in the access_token query string. The server must read it from there for /mentor-hub: see Choose the authentication scheme.
  • Confirmation. The widget shows the confirmation box. The answer goes to POST /mentor/approve and Stop to POST /mentor/cancel, on the hub's origin and with the same token. While a signed-in user's connection is open, only that user can send them. POST /mentor/approve answers 404 when nothing waits for the answer any more: it was answered already, the turn was stopped, or the connection closed. The widget treats 404 as "nothing left to answer". When the connection closes, the turn waiting for a confirmation ends with it. See Where nobody can answer for the other channels.
  • The hub is open until you protect it. configureTransports: e => e.RequireAuthorization() covers the hub and both /mentor/chat endpoints.
  • The widget never connects from another origin. The browser blocked the SignalR handshake. List the client's origin with AllowCredentials(), and use an absolute HubUrl. Call UseCors before UseAuthentication() and UseAuthorization(), and write those two calls in Program.cs yourself. Otherwise authorization can refuse the browser's preflight request.
  • The widget appears without styles for a moment. The stylesheet and script are missing from the host page.
  • After an upgrade, the tour does not start and voice output waits for the end of the answer. The browser still has an old MentorAgent.js: change the ?v= value.
  • Every page answers HTTP 500 after you pin the widget: "the required endpoints are not mapped on the server". The app was created with WebAssembly interactivity and has no Server interactivity: add AddInteractiveServerComponents() and AddInteractiveServerRenderMode(), as in Minimal configuration.
  • "Cannot create a component of type 'Novior.MentorAgent.Abstractions.Components.ChatWidget'… is not supported by WebAssembly rendering" in the browser console. The widget is pinned to the server inside an interactive router (Interactivity location: Global). Option A needs a static layout: with global interactivity, use option B.
  • The WebAssembly runtime does not start, and the console says CannotResolveService for HttpClient. The client project of a Blazor Web App registers no HttpClient, and the widget needs one for Stop and confirmations. Add the AddScoped(sp => new HttpClient { … }) line of the client snippet in Minimal configuration.
  • Every action with RequiredRoles is refused, and memory starts over at every reconnect. The server sees an anonymous caller: set AccessTokenProvider, and with JWT bearer read the token from the query string.
  • Stop or a confirmation seems ignored. The client has no HttpClient registered, or the server's CORS policy does not allow the POST or the Authorization header. The widget only logs the failure, in the browser console.
  • SerializeSessionAsync throws NotSupportedException. On WebAssembly the conversation lives on the server. Save and restore it over HTTP with GET and POST /mentor/session, as described in Any front end. The snapshot is opaque: store it as text and send it back unchanged.
  • Channels: how clients reach the agent — Which channel fits your client: the Blazor or MAUI widget, SignalR or SSE for JavaScript and mobile apps, and MCP or A2A for other agents and AI tools.
  • Blazor Server app — Configure MentorAgent in a Blazor Web App with Server interactivity: services, the options that matter, the widget and authentication.
  • Secure the endpoints — Protect the SignalR hub, Server-Sent Events, /mcp and /a2a endpoints with authentication and CORS before you expose them.
  • Configuration options — Every MentorOptions property with its type, default and effect, grouped by area, plus the options of the WebAssembly client and the YAML loader.