Elarion
JSON-RPC

MCP server

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

Elarion.AspNetCore.Mcp exposes the [RpcMethod] handlers 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 fully independent transport, a peer of the JSON-RPC and HTTP endpoints. It owns its own dispatcher (McpDispatcher, a dedicated JsonRpcDispatcher instance) built only from the handlers whose [RpcMethod(Transports = ...)] includes MCP. A handler therefore declares its method name once and chooses which transports expose it — JSON-RPC only, MCP only, or both (the default). 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.1.0" />
</ItemGroup>

Setup

The MCP server is wired through the generated ModuleBootstrapper (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;

var serializerOptions = CreateSerializerOptions(builder.Configuration);

// The public JSON-RPC endpoint (optional, independent of MCP): registers the /rpc dispatcher with the
// JSON-RPC-surfaced methods of enabled modules.
builder.Services.AddJsonRpc(serializerOptions, ModuleBootstrapper.RegisterRpcMethods);

// The MCP server: a dedicated dispatcher built from the MCP-surfaced methods, with one tool per handler.
builder.Services.AddElarionMcp(
    ModuleBootstrapper.GetMcpMetadata(builder.Configuration), // reflection-free, gated tool table
    serializerOptions,
    ModuleBootstrapper.RegisterMcpMethods,                    // builds the dedicated, gated MCP dispatcher
    o => o.ServerName = "MyApp");

var app = builder.Build();

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

GetMcpMetadata and RegisterMcpMethods are generated alongside RegisterRpcMethods by the source generator: a reflection-free tool table plus the registration delegate for the dedicated MCP dispatcher, both including only the MCP-surfaced handlers of enabled modules. A module disabled with Modules:{Name}:Enabled = false drops its tools entirely, and there is no assembly scanning at runtime.

MCP is self-contained: AddElarionMcp builds its own McpDispatcher and never touches the unkeyed /rpc JsonRpcDispatcher. MapElarionMcp() does not require MapJsonRpc() — 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;

[RpcMethod("clients.create")]
[Description("Creates a new client record.")]            // → tool description
public sealed class CreateClient(IAppDbContext 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.

Choosing which transports expose a handler

A handler declares its method name once with [RpcMethod] and selects its dispatcher-based surfaces with the RpcTransports flags (JsonRpc, Mcp, or All). The default is All — both JSON-RPC and MCP:

using Elarion.Abstractions;

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

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

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

Because the MCP dispatcher is built only from MCP-surfaced methods, an MCP-only handler is genuinely absent from /rpc and the exported JSON-RPC schema, and a JSON-RPC-only handler is never dispatchable 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 transports at once — see HTTP endpoints.

Renaming a tool

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

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

Options

builder.Services.AddElarionMcp(
    ModuleBootstrapper.GetMcpMetadata(builder.Configuration),
    serializerOptions,
    ModuleBootstrapper.RegisterMcpMethods,
    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 dedicated MCP dispatcher, in its own service scope (so scoped services such as a pooled DbContext are managed correctly) — exactly as a JSON-RPC request is dispatched, but isolated from the /rpc dispatcher. 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 methods collapse to the same tool name (for example a.b and a_b under the default transform), tool building throws at startup. Disambiguate with [McpMethod(ToolName = ...)] or a custom ToolNameTransform.

On this page