Questa pagina non è ancora tradotta in italiano: qui la trovi in inglese.
MCP server
Expose the methods of your .NET app as MCP tools for Claude Desktop, VS Code and other MCP clients, with authentication, role checks and writes withheld.
On this page
Use this page to expose your .NET app as a Model Context Protocol (MCP) server: your actions become tools for MCP clients such as Claude Desktop, VS Code or an agent you build elsewhere, with the same role checks as in your app. An MCP client lists the actions as tools and calls them over HTTP at /mcp.
When to choose it
Section titled “When to choose it”Choose it when another AI application should run your business logic: an AI assistant in the editor, a desktop client, an agent of your own.
- The client's model decides. MentorAgent runs the method the client calls, with no model, prompt or conversation of its own. Every call is a direct call to your code.
- Only actions that need no confirmation. Actions that need a confirmation are never published, and a
[MentorAction]that declares nothing asks by default: see Every declaration, on every channel. Actions with roles are published only to callers that hold one of the roles. See Security and roles. - Only Level 1 actions. The server publishes the methods of your action classes. The methods of a specialist, a class with
[MentorAgent], are never published, not even a read with no rules. To publish such a read, write it in a class without[MentorAgent]. The coordinator then calls it directly too, not through the specialist. - Any ASP.NET Core host. The server is in
Novior.MentorAgent.Server. A backend that callsAddMentorAgentServer()and a Blazor Server app that callsAddMentorAgentMcp()map it the same way.
If the other agent should talk to your whole agent, with your model, specialists and documents, see Agent-to-Agent (A2A) server. To call MCP servers from your agent, see MCP and A2A clients.
Install the packages
Section titled “Install the packages”.NET CLI
dotnet add package Novior.MentorAgent.ServerPackageReference
<ItemGroup>
<PackageReference Include="Novior.MentorAgent.Server" Version="1.2.0" />
</ItemGroup>Central Package Management
<ItemGroup>
<PackageVersion Include="Novior.MentorAgent.Server" Version="1.2.0" />
</ItemGroup><ItemGroup>
<PackageReference Include="Novior.MentorAgent.Server" />
</ItemGroup>File-based app
#:package Novior.MentorAgent.Server@1.2.0Novior.MentorAgent.Server brings Novior.MentorAgent and the MCP SDK for ASP.NET Core (2.3.0) with it. Add it to a Blazor Server app too: you do not need the hub for MCP. Neither package includes a model provider package. The code on this page uses OpenAI, so add its package too:
dotnet add package Microsoft.Extensions.AI.OpenAI --version 10.6.0For another provider, see Model providers.
Minimal configuration
Section titled “Minimal configuration”Set a model client, even if your server only publishes MCP tools. MentorAgent checks its options when the host starts, and without a
ChatClientor anAgentthe host does not start. An MCP call never reaches the model, so a server that only answers MCP clients never uses the key. MentorAgent does not check the key at startup either. You need a real key as soon as the same app also has a chat, such as the widget, the hub or an A2A server. Keep the key out of the source code:dotnet user-secrets init dotnet user-secrets set OpenAI:Key <your-key>Write the actions as usual, in a class you register in dependency injection:
public class CaseTools(ICaseStore store, ICaseQueue queue) { // For every caller. ReadOnly: MCP clients are told that it only reads [MentorAction( Description = "Gets the status of a support case", ReadOnly = true)] public Task<string> GetCaseStatus(string caseNumber) => store.GetStatusAsync(caseNumber); // Needs a signed-in user with the role [MentorAction( Description = "Lists the open cases assigned to a team", RequiredRoles = ["Supervisors"], ReadOnly = true)] public Task<IReadOnlyList<string>> ListTeamCases(string team) => queue.ListOpenCasesAsync(team); // Needs a person to confirm it [MentorAction( Description = "Closes a support case with a resolution", RequiresConfirmation = true)] public Task CloseCase(string caseNumber, string resolution) => store.CloseAsync(caseNumber, resolution); }The tool names are the method names in snake case:
get_case_status,list_team_cases,close_case.- The tool description is the
Descriptionof[MentorAction]. The parameters become the tool's input schema. ReadOnly = truesays that an action only reads. MCP clients receive it with a read-only hint.CloseCasedeclares neitherReadOnlynorRequiresConfirmation, so it asks for confirmation by default and the server does not publish it (it withholds it).[MentorAction]is in theNovior.MentorAgent.Attributesnamespace.ICaseStoreandICaseQueuestand for your own data access.
- The tool description is the
Turn the server on, register it and map the endpoint in
Program.cs:// AddMentorAgent, AddMentorAgentMcp, MapMentorAgentMcp using Novior.MentorAgent.Extensions; // MentorOptions (complete configuration) using Novior.MentorAgent.Options; using Microsoft.Extensions.AI; // AsIChatClient using OpenAI; // OpenAIClientbuilder.Services.AddScoped<CaseTools>(); builder.Services.AddMentorAgent(options => { options.AppName = "Case management"; // the MCP server's name 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]; options.McpServerEnabled = true; }); // Novior.MentorAgent.Server: registers the MCP server // (AddMentorAgentServer() calls it too) builder.Services.AddMentorAgentMcp(); // after var app = builder.Build(); app.MapMentorAgentMcp(); // /mcpAddMentorAgentMcp()registers the server whenMcpServerEnabledis on. Call it afterAddMentorAgent().AddMentorAgentServer()calls it for you. MentorAgent readsMcpServerEnabledwhenAddMentorAgentruns, so set it in that callback.The endpoint has no Cross-Origin Resource Sharing (CORS) policy. Desktop clients and editors are not browsers, so they need none. The MCP Inspector, a tool to test MCP servers, runs a proxy outside the browser, so it needs none either. A client that runs in a browser needs CORS: see the complete configuration.
Connect a client
Section titled “Connect a client”Run the app and point an MCP client that supports the Streamable HTTP transport at https://localhost:<port>/mcp, with the port your app listens on. Where you enter the address depends on the client.
- The client lists one tool,
get_case_status: the other two are withheld, as Security and roles explains. The server's name is yourAppName. - If the client refuses the ASP.NET Core development certificate, trust it with
dotnet dev-certs https --trust. - Once the endpoint requires authorization, as in the complete configuration, the client must send credentials your scheme accepts, usually an
Authorization: Bearerheader.
The two sections below connect VS Code and Claude Desktop to the app on your machine. They use the plain HTTP address that dotnet run prints, here http://localhost:5000: use the port of your app. To try them before you write any code, download the Cases MCP server sample, which listens on that address and needs no model key.
Connect VS Code
Section titled “Connect VS Code”VS Code reads the MCP servers of a workspace from .vscode/mcp.json. Add your app as a server of type http:
{
"servers": {
"cases": {
"type": "http",
"url": "http://localhost:5000/mcp"
}
}
}- See the tools. Run MCP: List Servers from the Command Palette, select
casesand start it. Show Output shows the connection, and the Tools section of Chat: Open Customizations lists one tool,get_case_status. Listing the tools calls no model. - Call a tool. The chat needs you signed in to VS Code with a GitHub account that has Copilot; Copilot Free is enough. In a chat session that uses tools, ask "What is the status of case C-1?". The model of your Copilot subscription chooses the tool, and VS Code asks you to approve the call before it runs, unless you approved the server's tools in advance with Chat: Manage Tool Approval.
- With authentication. Add a
headersobject to the server, for example with anAuthorizationheader.
Connect Claude Desktop
Section titled “Connect Claude Desktop”Claude Desktop reaches a remote server, a custom connector, from Anthropic's cloud, not from your computer. A connector needs a public HTTPS address, so it cannot reach localhost. For the app on your machine, Claude Desktop starts a local bridge instead: mcp-remote, a community npm package that is not part of MentorAgent or of Claude Desktop. It needs Node.js 18 or later.
Open Settings > Developer > Edit Config, which opens claude_desktop_config.json, and add the server:
{
"mcpServers": {
"cases": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://localhost:5000/mcp", "--allow-http"]
}
}
}- Where the file is. On Windows,
%APPDATA%\Claude\claude_desktop_config.json. On macOS,~/Library/Application Support/Claude/claude_desktop_config.json. - Restart Claude Desktop after you save the file. The bridge then lists
get_case_status, and Claude calls it when a question needs it. --allow-httplets the bridge use a plain HTTP address. Use it only for an app on your own machine.- After you deploy the app at a public HTTPS address, add it as a custom connector instead (Connectors, then Add custom connector), and protect the endpoint as in the complete configuration.
The tests of this documentation check that this JSON is valid and points at the MCP endpoint. Unlike the VS Code configuration, it has not been tried in Claude Desktop.
Complete configuration
Section titled “Complete configuration”This configuration is for real clients. It adds authentication and role-gated actions for the callers that may use them. For a client that runs in a browser, it also adds the origins that you allow.
builder.Services.AddScoped<CaseTools>();
builder.Services.AddMentorAgent(options =>
{
options.AppName = "Case management";
options.AppDescription =
"Support case management: case status, notes and closure";
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];
options.McpServerEnabled = true;
});
// Novior.MentorAgent.Server: registers the MCP server
// (AddMentorAgentServer() calls it too)
builder.Services.AddMentorAgentMcp();
// Browsers may call /mcp only from these origins. With the same name
// as MentorAgent's policy, yours is used
// A browser client also needs the session header
builder.Services.AddCors(cors => cors.AddPolicy("MentorAgentMcp", policy => policy
.WithOrigins("https://tools.example.com")
.AllowAnyHeader()
.AllowAnyMethod()
.WithExposedHeaders("Mcp-Session-Id")));
// Who is calling: an action with roles goes only to callers with the role
builder.Services.AddHttpContextAccessor();
builder.Services.AddOptions<MentorOptions>()
.Configure<IHttpContextAccessor>((options, http) =>
options.McpCallerPrincipal = () => http.HttpContext?.User);
// Plus your authentication scheme (JWT bearer, API gateway):
// builder.Services.AddAuthentication()...
// after var app = builder.Build();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.MapMentorAgentMcp(configure: e => e
.RequireAuthorization()
.RequireCors("MentorAgentMcp"));- CORS. The endpoint has no CORS policy, and a client that is not a browser needs none. A client that runs in a browser needs three things:
- Call
UseCorsbeforeUseAuthenticationandUseAuthorization, and write those two calls yourself. - Add
RequireCors("MentorAgentMcp")to the endpoint. - Register a policy with that name, which lists your origins and exposes the
Mcp-Session-Idheader. You can register it before or afterAddMentorAgentMcp(). If you do not, the policy thatAddMentorAgentMcp()adds allows any origin.
- Call
- Who may connect.
configure: e => e.RequireAuthorization()applies to the endpoint like any other convention, so a named policy works too. The authentication scheme is yours: Secure the endpoints shows JSON Web Token (JWT) bearer. - Who is calling.
McpCallerPrincipalhands MentorAgent the request's user. MentorAgent asks it once per request and does not keep the answer for the next caller. Connecting and seeing a role-gated action are two separate decisions: the first belongs toRequireAuthorization, the second toMcpCallerPrincipal. Roles are tested withClaimsPrincipal.IsInRole: see Security and roles.
What every call goes through
Section titled “What every call goes through”A call passes the same checks as a tool that the model calls.
- The result.
OnToolResultruns on the result before the client sees it. If the hook throws, the client gets anisErrorresult and not the data. - The limit. The call counts in the dashboard's top actions and against
RateLimitPerUser. MCP calls have an allowance of their own, apart from chat messages. - Who is counted.
- A caller is counted by the
NameIdentifierclaim or, if that claim is missing, bysub,oidor the name. - A caller with none of them is counted by its remote address, if you call
AddHttpContextAccessor().AddMentorAgentServer()andAddMentorAgentA2A()already call it. Without the accessor, all such callers share one allowance. - Behind a proxy, use the forwarded headers middleware (external site). Otherwise every caller looks like the proxy.
- A caller is counted by the
- The log. Each call writes one Information line to the log: the tool, the outcome, the time and a fingerprint of the caller. The caller's id never appears.
Options for the MCP server
Section titled “Options for the MCP server”| Option | Why it matters for the MCP server |
|---|---|
McpServerEnabled | Turns the server on. AddMentorAgentMcp() then registers it and the MentorAgentMcp CORS policy, which no endpoint uses until you add RequireCors. Without the flag, MapMentorAgentMcp() stops the app at startup. MentorAgent reads it when AddMentorAgent runs |
AppName | The server name that MCP clients show. MentorAgent reads it when AddMentorAgent runs: set it in that callback |
McpServerPath | Where MapMentorAgentMcp() maps the endpoint when you pass no path. A path passed to the method wins |
McpCallerPrincipal | Resolves the caller, once per request. Roles are tested with IsInRole. Without it, every action with RequiredRoles is withheld |
RequiresApproval | An action it matches is withheld, like one with RequiresConfirmation |
RateLimitPerUser, RateLimitWindowSecs | Limit the MCP calls of each caller, in an allowance of their own. For a limit on the requests themselves, use ASP.NET Core rate limiting, as in Secure the endpoints |
MaxMessageLength, EnableSafetyCheck | No effect: an MCP call runs your method without a turn |
OnToolResult | Changes or redacts the result of every MCP call before the client sees it |
IsReadOnlyAction | Declares by tool name the actions that only read, like ReadOnly = true. MCP clients receive them with readOnlyHint. A [MentorAction] method that this option names does not ask for confirmation by default |
RequireConfirmationForWrites | On by default: a [MentorAction] that declares nothing asks for confirmation, so the server withholds it. With false, only RequiresConfirmation = true withholds an action |
McpServers | Does not apply here: it connects your agent to other MCP servers. See MCP and A2A clients |
Every option is in Configuration options, and the MCP ones under MCP.
Security and roles
Section titled “Security and roles”An MCP request has no person on the other side to answer a confirmation, and no signed-in user unless you say who it is. So MentorAgent publishes only what it can check:
| Action | Without McpCallerPrincipal | With McpCallerPrincipal |
|---|---|---|
A read with no roles, such as get_case_status (ReadOnly = true) | Published | Published |
RequiredRoles, such as list_team_cases | Withheld | Published only to an authenticated caller holding one of the roles |
An action that asks for confirmation, such as close_case | Withheld | Withheld, always |
Which declarations ask, and which are published with readOnlyHint, is in Every declaration, on every channel.
- Withheld means absent. The tool is not in the list, and a call by name runs nothing: the client gets
Unknown tool: '…'withisError, the same answer as for a name that never existed. WhenMapMentorAgentMcp()maps the endpoint at startup, a warning in the log names every withheld action. A write that declared nothing is named too, as(approval). The tools themselves are still built at the first request. - The endpoint is open until you protect it. Anyone who reaches the port can list and call every published tool. Each call runs your code, so treat it like a public API.
- Per-record checks are your code. A role says who may call the action, not which records the caller may read. Check records in your methods, or in an action policy, which the MCP server applies too: see What stays in your code.
- Roles are tested with
IsInRole. It reads the claim type that the identity names as itsRoleClaimType. A token with arolesclaim works when the identity says so. Microsoft.Identity.Web sets the identity up that way. With JWT bearer andMapInboundClaims = false, setTokenValidationParameters.RoleClaimTypetoroles. OtherwiseIsInRolelooks forClaimTypes.Role, and every role-gated action stays withheld. - A read-only declaration reaches the client. Some MCP clients skip their own confirmation for a tool with
readOnlyHint, so a write declared read-only by mistake can run from them without anyone being asked. - A published write runs unattended. A write that declares
RequiresConfirmation = false, or a method with only[Description], can be run by any MCP client without anyone being asked. Publish one only when that is what you want.
The same rules on the other channels are in Where nobody can answer.
Common errors
Section titled “Common errors”Every request to
/mcpanswers 500, and the log says that a middleware supporting CORS was not found. The endpoint hasRequireCors(...)and the pipeline has noapp.UseCors(). Add it beforeUseAuthentication()andUseAuthorization(), and write those two calls inProgram.csyourself. Or removeRequireCorsif no browser calls the endpoint.The app stops at startup with "Cannot map MCP endpoint: McpServerEnabled is false". Set
options.McpServerEnabled = trueinAddMentorAgent, or remove the call.The app stops at startup with "Cannot map the MCP endpoint: the MCP server is not registered". Nothing registered the server. Call
AddMentorAgentMcp()afterAddMentorAgent(), or callAddMentorAgentServer().The app stops at startup with "McpServerEnabled is true, but AddMentorAgentMcp() ran before AddMentorAgent()". Call
AddMentorAgentMcp()afterAddMentorAgent().The app stops at startup with "McpServerEnabled is true, but it was false when AddMentorAgent() read it". Set
McpServerEnabledin theAddMentorAgentcallback. A laterConfigure<MentorOptions>or a configuration binding comes too late. See Troubleshooting.The build does not find
AddMentorAgentMcporMapMentorAgentMcp. Both methods are in theNovior.MentorAgent.Serverpackage. Add it to the project, as in Install the packages.The client lists fewer tools than your class has. The missing ones are withheld: the warning in the log names them, and Every declaration, on every channel says which declarations are withheld. If the warning does not name a tool, there are four other causes:
- The class has
[MentorAgent]: the server does not publish the methods of a specialist, and the warning does not name them. - The class is listed in the
Toolsof a team member. Those methods belong to the member, and an Information line in the log says so. - The method is inherited, static, or has an empty
[Description]. MentorAgent finds only public instance methods declared on the class itself. - The class is not registered in dependency injection, and the log says so with "Could not build MCP server tool".
- The class has
A call gets "Unknown tool: '…'. This server does not offer it". The server does not list that tool for this caller. Nothing runs, and the call is not logged as an error. There are four possible reasons:
- the action needs confirmation;
- the caller does not have the role that the action requires;
- the method belongs to a specialist (see When to choose it);
- the tool name does not exist.
If the client uses an old tool list, or its model guessed a name, list the tools again. If the tool is withheld, see the items on missing tools and on role-gated actions.
A call returns
isErrorwith "Rate limit reached: at most … tool calls every … s". The caller made more calls thanRateLimitPerUserallows withinRateLimitWindowSecsseconds. A call to an unknown tool counts too.A call returns
isErrorwith "The tool ran, but its result could not be returned". YourOnToolResulthook threw. The action did run, and the log has a warning with the exception.A role-gated action never appears, even for an administrator. There are three possible causes:
McpCallerPrincipalis not set.- The request is not authenticated.
- The roles are in a claim that the identity does not name as its
RoleClaimType. An example is arolesclaim with JWT bearer andMapInboundClaims = false.
A browser blocks every request to
/mcp. The endpoint has no CORS policy until you addRequireCors("MentorAgentMcp"). See the CORS item in the complete configuration.Errors about a
DbContextthat is disposed or used by two threads. The action class is shared by every call: injectIDbContextFactory<TContext>.The client gets
401. The endpoint requires authorization and the client does not send credentials your scheme accepts.
Vedi anche
Sezione “Vedi anche”- Canali: come i client arrivano all'agente — Scegli come i client raggiungono il tuo agente: il widget della chat, SignalR o HTTP dal tuo front end, o un protocollo per altri agenti e strumenti AI.
- A2A server — Let other AI agents call your .NET agent over the Agent-to-Agent (A2A) protocol: the agent card, authentication, and the actions that need confirmation.
- 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.
