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.
Search
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.
Entity Search
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.McpTools→MyApp) and participate inFilterToolsfiltering
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):
- Strip
.McpToolssuffix if present - If the result contains
.Model., the first segment after.Model.becomes the feature - 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
- Benevia.Core.MCP.Generator — source generator details
- Benevia.Core.API — OData API infrastructure
- Model documentation — entity definition guide