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.