Elarion

MCP server

Expose your handlers as a Model Context Protocol (MCP) server, independent of the JSON-RPC HTTP endpoint.

Elarion.AspNetCore.Mcp exposes the [Handler] operations you already write as tools on an MCP server, over Streamable HTTP. It is the only Elarion package that references the ModelContextProtocol SDK, so plain JSON-RPC hosts never pull it in.

MCP is a peer of the JSON-RPC and HTTP endpoints, but it is not a separate stack. JSON-RPC and MCP are both thin adapters over one transport-neutral bus — the HandlerDispatcher (in Elarion.Abstractions) that maps an operation name to a handler and invokes it through the full decorator pipeline. McpDispatcher (in Elarion.JsonRpc.Mcp) is the MCP adapter: it serves only the operations whose [Handler(Transports = ...)] flag includes HandlerTransports.Mcp, surfacing each as a tool. A handler therefore declares its operation once and chooses which surfaces expose it — JSON-RPC only, MCP only, or both (the default) — and the generator builds a single registry both adapters resolve. You never have to mount (or even register) the public JSON-RPC endpoint to use MCP.

Package reference

<ItemGroup>
  <PackageReference Include="Elarion.AspNetCore.Mcp" Version="0.2.6" />
</ItemGroup>

Setup

The MCP server is wired through the generated ElarionBootstrapper (modules are the single hosting path), which exposes the gated MCP peers alongside the JSON-RPC and HTTP ones:

using Elarion.AspNetCore;
using Elarion.AspNetCore.Mcp;

// The public JSON-RPC endpoint (optional, independent of MCP): the JSON-RPC adapter over the shared bus,
// serving the JSON-RPC-surfaced operations of enabled modules at /rpc.
builder.Services.AddElarionJsonRpc(ElarionBootstrapper.RegisterHandlers);

// The MCP server: the MCP adapter over the same bus, serving the MCP-surfaced operations as one tool each.
// Both adapters read the canonical IElarionJsonSerialization (contributed by AddElarion).
builder.Services.AddElarionMcp(
    builder.Configuration.GetMcpMetadata(), // reflection-free, gated tool table
    ElarionBootstrapper.RegisterHandlers,    // the single, gated handler registry both adapters resolve
    o => o.ServerName = "MyApp");

var app = builder.Build();

app.MapElarionMcp();   // mounts /mcp — calling MapElarionJsonRpc() is optional and independent
app.Run();

GetMcpMetadata is generated alongside RegisterHandlers by the source generator: a reflection-free tool table plus the single handler registration delegate (RegisterHandlers) that builds the one shared registry both adapters filter by transport flag. The MCP adapter sees only the MCP-surfaced operations of enabled modules. A module disabled with Modules:{Name}:Enabled = false drops its tools entirely, and there is no assembly scanning at runtime.

MCP and JSON-RPC are two adapters over one HandlerDispatcher registry, each filtered by the Transports flag — not two separate dispatcher instances. AddElarionMcp builds the McpDispatcher adapter, serving only MCP-flagged operations. MapElarionMcp() does not require MapElarionJsonRpc() — you can expose MCP without exposing (or even registering) the public JSON-RPC endpoint at all.

Tool and parameter descriptions

Descriptions come from [System.ComponentModel.Description], read at compile time:

using System.ComponentModel;
using Elarion.Abstractions;

[Handler("clients.create")]
[Description("Creates a new client record.")]            // → tool description
public sealed class CreateClient(AppDbContext db)
    : IHandler<CreateClient.Command, Result<CreateClient.Response>> {
    public sealed record Command {
        [Description("Human-readable client name.")]      // → input-schema property description
        public required string DisplayName { get; init; }
    }

    public sealed record Response(Guid Id);
}

The generator records the .NET property name; the JSON name is resolved at startup from your serializer's PropertyNamingPolicy, so descriptions attach correctly under any naming policy.

Tool input schemas also carry the request DTO's DataAnnotations constraints ([Range], the length attributes, [RegularExpression], [EmailAddress], [Url], [Base64String]) as JSON Schema keywords — they share the JSON-RPC schema builder, so an agent sees the same shape contract the server enforces. See Validation.

Choosing which transports expose a handler

A handler declares its operation once with [Handler] (the name is optional — see name inference) and selects its adapter surfaces with the HandlerTransports flags (JsonRpc, Mcp, or All). The default is All — both JSON-RPC and MCP:

using Elarion.Abstractions;

[Handler("clients.create")]                                       // both (default)
public sealed class CreateClient : IHandler<...> { ... }

[Handler("clients.list", Transports = HandlerTransports.JsonRpc)] // JSON-RPC only — absent from MCP
public sealed class ListClients : IHandler<...> { ... }

[Handler("ai.summarize", Transports = HandlerTransports.Mcp)]     // MCP only — absent from /rpc and the schema
public sealed class Summarize : IHandler<...> { ... }

Both adapters resolve the same HandlerDispatcher registry, filtered by each operation's transport flag: the MCP adapter serves only MCP-flagged operations, so an MCP-only handler is genuinely absent from /rpc and the exported JSON-RPC schema, and a JSON-RPC-only handler is never surfaced as a tool.

REST is a separate opt-in via [HttpEndpoint], since it needs route, verb, and parameter binding that don't fit a flags enum. A handler can be on all three attribute-selected transports at once — see HTTP endpoints.

Renaming a tool

The optional [McpHandler] attribute customizes the MCP projection — currently the tool name. It is purely additive; absent, the tool name is derived from the operation name via ToolNameTransform.

[Handler("clients.create"), McpHandler(ToolName = "create_client")]
public sealed class CreateClient : IHandler<...> { ... }

Options

builder.Services.AddElarionMcp(
    builder.Configuration.GetMcpMetadata(),
    ElarionBootstrapper.RegisterHandlers,
    o => {
        o.ServerName = "MyApp";                            // required
        o.ServerVersion = "1.0";
        o.EndpointPath = "/mcp";
        o.ToolNameTransform = m => m.Replace('.', '_');    // default
        o.IncludeErrorDetails = true;                      // surface JSON-RPC error code/data as structured content
    });

MapElarionMcp() returns the endpoint builder, so you apply authorization yourself:

app.MapElarionMcp().RequireAuthorization();

How tool calls work

Each tool call runs through the MCP adapter onto the shared HandlerDispatcher, in its own service scope (so scoped services such as a pooled DbContext are managed correctly) — the same handler and decorator pipeline a JSON-RPC request invokes, just entered through the MCP adapter instead of the JSON-RPC one. A successful handler result is returned as JSON text; a failure maps to an MCP error, preserving the JSON-RPC error code and data as structured content when IncludeErrorDetails is enabled.

If two operations collapse to the same tool name (for example a.b and a_b under the default transform), tool building throws at startup. Disambiguate with [McpHandler(ToolName = ...)] or a custom ToolNameTransform.

On this page