Skip to content
kc@kumarChandrachooda.com:~$ cd /blog/your-endpoints-are-now-tools && read --section="top" 0%
.NET

Your Endpoints Are Now Tools

FastEndpoints.Mcp exposes opt-in endpoints as Model Context Protocol tools - schemas generated from your DTOs and enriched from your validators, a synthetic HttpContext that runs the real pipeline, and a visibility model that deliberately ignores your REST auth. Part 11 of FastEndpoints — The Missing Chapters.

By Kumar Chandrachooda 23 Jun 2026 5 min read

The newest additions to the FastEndpoints repository assume something no part of the parent series imagined: that the caller of your API might be a language model. Src/Agents holds two young packages (1.0.0-beta.3, versioned apart from the core library) - FastEndpoints.Mcp, which exposes endpoints as Model Context Protocol tools, and FastEndpoints.A2A, which exposes them as agent-to-agent skills (next week's closer). This part reads the MCP half, and the shared machinery underneath both - which turns out to be the best guided tour of FastEndpoints' own pipeline that anyone has written, because it had to reimplement calling it.

Opt-in, or nothing

Wiring is two calls straddling the usual pair, with UseMcp required to come after UseFastEndpoints so the endpoint registry exists:

bld.Services.AddFastEndpoints().AddMcp();
var app = bld.Build();
app.UseFastEndpoints().UseMcp("/mcp", route => route.RequireAuthorization());

That maps an MCP server (built on Microsoft's official ModelContextProtocol.AspNetCore SDK) at /mcp - and it exposes precisely nothing. Every tool is opt-in, per endpoint, either in Configure():

public override void Configure()
{
    Get("/inventory/{Sku}");
    this.McpTool("inventory_lookup", "Reads current stock for a SKU.",
        t => { t.Title = "Inventory Lookup"; t.Hints.ReadOnly = true; t.Hints.Idempotent = true; });
}

or as a [McpTool(...)] class attribute. The integration seam is a design decision worth copying: the extension stores its McpToolInfo on the EndpointDefinition's public metadata bag - the core library needed zero modifications to grow an MCP face. The same bag-and-attributes pattern any third party could have used; the maintainers just used it themselves.

Those Hints mirror MCP's tool annotations - ReadOnly, Idempotent, Destructive, OpenWorld, all nullable booleans, because “not stated” and “false” mean different things to an agent deciding whether a tool is safe to retry. The attribute form has a subtle problem there: attribute properties can't be nullable, so how do you tell Destructive = false from never-set? The implementation reads GetCustomAttributesData().NamedArguments - the syntactic record of which properties the attribute author actually wrote - and preserves exactly those. A false you wrote survives; a false you didn't write stays null. Reflection archaeology in service of three-valued logic.

The schema is your DTO plus your validator

An MCP tool needs a JSON Schema for its inputs. The package builds it from the request DTO via .NET's own JsonSchemaExporter, honoring the endpoint's serializer context - if your DTOs serialize snake_case through a source-generated context, the tool schema and the invocation payloads both follow. Then two passes make it a FastEndpoints schema rather than a generic one.

First, subtraction: properties an agent must never supply are removed from the schema and refused at invocation - [HasPermission] booleans, required [FromClaim] props, header/cookie-bound props hidden from docs. Your claims-derived fields don't become prompt-injectable parameters. The schema also sets additionalProperties: false, and unknown argument keys fail as validation errors instead of being dropped - loud rejection over silent tolerance, house style.

Second, addition: a FluentValidationSchemaEnricher reflects over the endpoint's validator - resolved from a fresh DI scope, so scoped validator dependencies work - and projects the expressible subset of rules into the schema: NotEmpty to required, Length to minLength/maxLength, Matches to pattern, comparisons to minimum/maximum, EmailAddress to format: email. Rules it can't express (Must, cross-property predicates) are silently skipped, and the doc comments make the right observation: they still run at request time. The schema is the advertisement; the validation pipeline remains the law. Response DTOs get an output schema too (skippable via IncludeOutputSchemas), minus any [ToHeader] properties - those leave over HTTP headers, which an MCP result doesn't have.

A synthetic HttpContext runs the real pipeline

The shared engine both agent packages compile in (from Src/Agents/Shared - deliberately internal source-sharing, not a third NuGet package, so consumers see only FastEndpoints.Mcp on the wire) is EndpointInvoker, and its job description is audacious: execute an endpoint without HTTP routing while keeping every behaviour of a real request.

It does it by manufacturing the world. An AgentRequestBuilder translates the tool call's flat JSON arguments into a correctly shaped request - route parameters substituted into the template (constraints like {id:int} stripped, catch-alls and optionals handled), query-vs-body decided by verb, headers and cookies placed, nested objects flattened to key.child and key[index] pairs, all against cached per-endpoint specs. A factory then builds a DefaultHttpContext around it - JSON body stream, route values, a synthetic endpoint feature carrying the real EndpointDefinition - opens a DI scope, temporarily swaps the ambient IHttpContextAccessor to the synthetic context (restored in a finally - remember how much resolution flows through that accessor), and runs the standard endpoint execution: binder, validators, processors, handler, disposal.

One subtlety in the result classification deserves its sentence: the invoker reports validation failures even when the endpoint returned 200, because an endpoint using DontThrowIfValidationFails() would otherwise hand the agent a success wrapping errors it cannot see. Machines need the failure channel more explicitly than humans do.

Results map by status: success returns text content plus structured content - which the package first validates against its own advertised output schema, silently omitting the structured form if the response drifted from the contract (the text still flows; the promise is never broken silently). HTTP errors come back as isError payloads with status and body. And unhandled exceptions are logged server-side but cross the wire as a generic "Endpoint invocation failed." - stack traces are for your logs, not for someone else's model.

Two auth systems, one firewall

The decision most likely to surprise: your endpoint's REST authorization is deliberately not reused for tool visibility. Instead McpOptions has a ToolVisibilityFilter - (EndpointDefinition, ClaimsPrincipal, HttpContext) => bool - defaulting to authenticated-callers-only, evaluated both when listing tools and again on every call (a tool that just became invisible fails like one that never existed). The rationale reads clearly from the shape: “this user may call POST /orders” and “this user's agent should see an order-creation tool” are different policies with different owners, and conflating them would let REST conventions accidentally publish tools. You relax it explicitly - o.ToolVisibilityFilter = (_, _, _) => true in a dev harness - or tighten it per tenant.

My favourite small find is the workaround comment: the MCP spec allows / in tool names, but the current SDK rejects slashes at construction - so the package constructs the tool under a sanitized name, then reaches into the protocol object afterward and puts the real name back. Working around your own platform's SDK, politely, in two lines.

The through-line of this chapter: FastEndpoints spent years building a disciplined front door for human-written clients, and the MCP package proves the discipline transfers - schemas from DTOs, contracts from validators, the same pipeline under a different protocol. Next week the final chapter: the A2A package, a JSON-RPC dispatcher written by hand, and the close of both series.

Which CV would you like?

Pick the one closest to the role. Each is two pages and carries the same facts.

Applied AI

Agentic AI & Cloud Engineer · multi-agent systems, RAG and MCP on AWS

Cloud & .NET

C#, .NET 10 and ASP.NET Core microservices on AWS and Azure · Terraform & AWS CDK

Data Engineering

AWS lakehouse · Kinesis, Glue (PySpark), Apache Iceberg, Lake Formation

Everything

All three domains, with the standout project from each