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.
gRPC
Add unary and request-driven server-streaming gRPC service methods over Elarion handlers with explicit protobuf mapping, scoped identity, and stable AppError status translation.
Writing a custom transport
Invoke Elarion handlers from gRPC, a console, a queue, or another host while preserving scoped identity and the full handler pipeline.