Benevia.Core.MCP

Introduction

Benevia.Core.MCP provides Model Context Protocol (MCP) server infrastructure for exposing functionality to AI clients. It supports two kinds of tools:

  • Generated entity tools — add [McpEntity] to an entity and the companion source generator produces Get, List, Create, Update, and Delete tools automatically
  • Custom tools — add [McpServerToolType] to any class to create hand-written tools with full DI support

Both kinds are auto-discovered, registered in the same ToolRegistry, and participate in dynamic toolset filtering.

Note: Generated entity tools require Benevia.Core.Events for business logic. Custom tools only need a reference to Benevia.Core.MCP.

Getting Started

1. Install the NuGet packages

dotnet add package Benevia.Core.MCP
dotnet add package Benevia.Core.MCP.Generator  # only needed if using [McpEntity]

The generator package must be added as an analyzer to your model project:

<PackageReference Include="Benevia.Core.MCP" Version="$(BeneviaCoreMcpVersion)" />
<PackageReference Include="Benevia.Core.MCP.Generator" Version="$(BeneviaCoreMcpGeneratorVersion)"
                  OutputItemType="Analyzer" ReferenceOutputAssembly="false" />

2. Mark entities for MCP exposure

Add [McpEntity] to any [ApiEntity] class you want to expose via MCP:

[ApiEntity]
[McpEntity]
[NaturalKey(nameof(Sku))]
public partial class Product : EntityBase
{
    [Required]
    [MaxLength(20)]
    [Property<DataTypes.Text>("Sku")]
    public partial string Sku { get; set; }

    [Property<DataTypes.Text>("Sales description", Description = "Sales-facing product description")]
    public partial string SalesDescription { get; set; }

    [Property<DataTypes.Decimal>("Cost")]
    public partial decimal Cost { get; set; }
}

Read tools (Get and List) are always generated. To exclude write operations:

// Read-only: no Create, Update, or Delete tools
[McpEntity(ExcludeOperation.Create, ExcludeOperation.Update, ExcludeOperation.Delete)]
public partial class AuditLog : EntityBase { ... }

// Allow Create and Update, but prevent Delete
[McpEntity(ExcludeOperation.Delete)]
public partial class PriceLevel : EntityBase { ... }

3. Register the MCP server

In your Program.cs:

using Benevia.Core.MCP;

builder.Services
    .AddCoreMcpServer()
    // ... other services
    ;

app.UseCoreMcp();
app.UseCoreApi();
app.Run();

The MCP endpoint is mapped to /mcp with Bearer token authentication.

4. Connect an MCP client

Any MCP-compatible client can connect to your server:

Endpoint: http://localhost:5090/mcp
Transport: Streamable HTTP (POST with SSE responses)
Auth: Authorization: Bearer <access-token>

Generated Tools

For each [McpEntity] class, the generator produces:

Tool Description Excludable
Get[Entity] Retrieve by GUID or natural key No
List[Entity] Paginated list with filter/search No
Create[Entity] Create a new record Yes
Update[Entity] Partial update by GUID or natural key Yes
Delete[Entity] Delete by GUID or natural key Yes

Filtering

List tools accept a filter parameter using Dynamic LINQ syntax:

Cost > 100
Name.Contains("test")
Cost > 50 && Status != "Discontinued"

Only entity properties are allowed in filters (enforced by a whitelist). Invalid syntax returns an error.

Paging

List tools support skip and top parameters:

Parameter Default Max
skip 0 -
top 100 1000

Responses include a Paging object with TotalCount, Skip, Top, and HasMore fields.

List tools accept a search parameter that delegates to an IEntitySearch implementation. The default is a no-op. To enable search, provide your own IEntitySearch (see Customization below).

Schema Resources

The MCP server exposes resources for client discovery:

URI Description
resource://erp/schemas JSON schemas for all exposed entities
resource://erp/schemas/{entityType} Schema for a specific entity type
resource://erp/relationships Foreign key relationships between entities

Dynamic Toolsets (Advanced)

By default, all tools are registered at startup. For large models, enable dynamic toolsets to let clients select which features to load:

builder.Services.AddCoreMcpServer(useDynamicToolsets: true);

This adds a FilterTools meta-tool that clients use to enable/disable tools by feature or entity type.

Customization

Property Descriptions

Use the Description named argument on [Property<T>] to provide tool parameter descriptions:

[Property<DataTypes.Decimal>("Cost", Description = "Unit cost in USD")]
public partial decimal Cost { get; set; }

These descriptions appear in the generated tool schemas, helping AI clients understand field semantics.

Implement IEntitySearch to provide full-text search:

public class MyEntitySearch(ISearchExpressionBuilder builder) : IEntitySearch
{
    public IQueryable<T> Apply<T>(IQueryable<T> query, string searchText)
    {
        var expression = builder.BuildSearchExpression(typeof(T), searchText);
        if (expression is Expression<Func<T, bool>> typed)
            return query.Where(typed);
        return query;
    }
}

AddCoreMcpServer() auto-discovers IEntitySearch implementations from referenced assemblies. If your implementation is in a separate assembly or you want explicit control, register it manually:

builder.Services.AddScoped<IEntitySearch, MyEntitySearch>();

Custom Tools

You can create hand-written MCP tools alongside the generated entity tools. Mark a class with [McpServerToolType] in any assembly that references Benevia.Core.MCP:

using System.ComponentModel;
using Benevia.Core.MCP;
using ModelContextProtocol.Server;

namespace MyApp.McpTools;

[McpServerToolType]
public class ReportTools(IReportService reports)
{
    [McpServerTool(Name = "RunSalesReport", ReadOnly = true, UseStructuredContent = true)]
    [Description("Generates a sales report for a date range.")]
    public ToolResult<SalesReportDto> RunSalesReport(
        [Description("Start date (yyyy-MM-dd)")] string startDate,
        [Description("End date (yyyy-MM-dd)")] string endDate)
    {
        var result = reports.Generate(startDate, endDate);
        return new ToolResult<SalesReportDto>(result);
    }
}

Custom tools are auto-discovered at startup and work in both static and dynamic toolset modes:

  • Static mode: custom tools appear alongside generated entity tools
  • Dynamic mode: custom tools get a feature name derived from their namespace (e.g., MyApp.McpToolsMyApp) and participate in FilterTools filtering

Important: Use instance methods for tools that need DI services. Constructor parameters and method parameters are resolved through dependency injection. Static methods cannot use constructor injection.

Return types

Use the types from Benevia.Core.MCP to match the response shape of generated tools:

Return type When to use
ToolResult<T> Single-item results. Serializes as { data: ..., validationErrors: [...] }
PagedToolResult<T> List results. Serializes as { data: [...], paging: { totalCount, skip, top, hasMore } }
string Simple text responses that don't need structured output

[McpServerTool] properties

Property Default Purpose
Name method name Tool name visible to MCP clients
ReadOnly false true for queries that don't modify data
Destructive false true for delete operations
UseStructuredContent false true to return typed objects as structured JSON. Required when returning ToolResult<T> or PagedToolResult<T>

Descriptions

Use [Description] on the method and on each parameter. These are the primary way AI clients understand what a tool does and what each parameter means.

Feature derivation

The namespace determines which feature group a custom tool belongs to in dynamic toolset mode. The derivation rules (applied in order):

  1. Strip .McpTools suffix if present
  2. If the result contains .Model., the first segment after .Model. becomes the feature
  3. Otherwise, the full remaining namespace is used
Namespace Feature
MyApp.Model.Reports.McpTools Reports
MyApp.McpTools MyApp
Benevia.ERP.API.McpTools Benevia.ERP.API

Architecture

flowchart TD
    A["[McpEntity] on model class"] --> B[MCP.Generator]
    B --> C[Generated Tools + DTOs + Registry]
    C --> D[ToolRegistry]
    I["[McpServerToolType] custom tools"] --> D
    D --> E["/mcp endpoint"]
    E --> F[MCP Client]
    G[IEntitySearch] --> E
    H[IEntityFilter] --> E

The generator runs at compile time and produces tool classes from [McpEntity] entities. Custom [McpServerToolType] classes are discovered at startup. Both are registered in the ToolRegistry and served through the /mcp endpoint.

More Info