General
Why Your .NET MCP Tool Classes Crash at Runtime (And the Two-Line Fix)
Gagan Bajaj DEV Community 周榜
3 views
If you're building MCP servers in .NET with instance-based tool classes — the kind that take ILogger<T>, HttpClient, or a repository through constructor injection — there's a good chance you've already hit this at runtime:
System.Reflection.TargetException: Non-static method requires a target.
No warning at startup. No compile-time error. Just a crash the moment an AI model calls your tool in production.
This post explains why it happens, shows the two-line fix in DotnetFastMCP v2.1, and covers a related improvement that makes AI models call your tools correctly on the first attempt — without asking the user for clarification.
The Problem: Two Paper Cuts That Bled Every Day
Paper Cut 1: The Runtime Crash Nobody Warned You About
The framework lets you organize your MCP tools into classes and scan them with one line:
builder.WithComponentsFrom(Assembly.GetExecutingAssembly());
This scans your assembly, finds every method decorated with [McpTool], and registers it. Clean, simple, plug-and-play.
Except — if your tool class had constructor injection, you'd get a silent registration. The class would appear in the tool list. But the moment an AI model called it, the server would throw:
System.Reflection.TargetException: Non-static method requires a target.
At runtime. In production. With no warning during startup.
The cause was straightforward: WithComponentsFrom registered the methods in the tool dictionary, but never registered the declaring class in ASP.NET Core's DI container. When the handler tried to resolve an instance for invocation, there was nothing to resolve.
The workaround was to manually add every tool class:
// ❌ v2.0: manual registration required for every non-static tool class
builder.Services.AddTransient<ProductSearchTool>();
builder.Services.AddTransient<ImageGenerationTool>();
builder.Services.AddTransient<InventoryTool>();
// ... one line per class, forever
This is exactly the opposite of what "plug-and-play" should mean.
Paper Cut 2: AI Models Were Flying Blind on Parameter Names
When an AI model calls tools/list, the server returns a JSON Schema for each tool's parameters. Here's what that schema looked like in v2.0:
{
"name": "search_products",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string" },
"limit": { "type": "integer" },
"filter": { "type": "string" }
},
"required": ["query"]
}
}
No descriptions. Just types and names. The model had to guess what filter means. Is it a category? A regex? A SQL WHERE clause?
The Fix: Two Lines of Setup, Zero Boilerplate
Both issues are fixed in v2.1. Install the update:
dotnet add package DotnetFastMCP --version 2.1.1
Fix 1: Constructor Injection Just Works
Starting in v2.1, WithComponentsFrom automatically registers every non-static tool class in the DI container using TryAddTransient:
public class ProductSearchTool
{
private readonly IProductRepository _repo;
private readonly ILogger<ProductSearchTool> _logger;
// Constructor injection — works automatically in v2.1
public ProductSearchTool(IProductRepository repo, ILogger<ProductSearchTool> logger)
{
_repo = repo;
_logger = logger;
}
[McpTool("search_products", Description = "Searches the product catalog")]
public async Task<string> SearchAsync(string query, int limit = 10)
{
_logger.LogInformation("Searching for '{Query}'", query);
var results = await _repo.SearchAsync(query, limit);
return JsonSerializer.Serialize(results);
}
}
Your Program.cs stays clean — register only the dependencies, not the tool class:
var mcpServer = new FastMCPServer("my-server");
var builder = McpServerBuilder.Create(mcpServer, args);
builder.Services.AddScoped<IProductRepository, SqlProductRepository>();
builder.WithComponentsFrom(Assembly.GetExecutingAssembly());
var app = builder.Build();
await app.RunMcpAsync(args);
TryAddTransient means existing registrations (like AddHttpClient<T>) are preserved — no collisions.
Fix 2: [McpDescription] — Tell the AI What Each Parameter Means
[McpTool("search_products", Description = "Searches the product catalog")]
public async Task<string> SearchAsync(
[McpDescription("The search query string, e.g. 'red cotton saree'")] string query,
[McpDescription("Maximum number of results to return (1–100)")] int limit = 10,
[McpDescription("Filter by category: 'clothing', 'accessories', 'footwear'")] string? category = null)
{
...
}
Now tools/list returns descriptions in the schema. The model knows exactly what to pass without asking the user.
Framework-injected parameters (McpContext, CancellationToken, ClaimsPrincipal) are always excluded automatically.
Try It Yourself: Verify in 60 Seconds
git clone https://github.com/tekspry/DotnetFastMCP.git
cd DotnetFastMCP/examples/BasicServer
dotnet run -- --urls http://localhost:5100
Then call the non-static tool:
curl -s -X POST http://localhost:5100/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"greet_user","arguments":{"name":"Alice","style":"formal"}}}' | jq .
Response: "Good day, Alice. How may I assist you today?"
No manual DI registration. No runtime crash. Constructor injection just works.
Migration from v2.0
No breaking changes. Update the package version, remove manual AddTransient<MyToolClass>() calls (now redundant), and add [McpDescription] to your parameters.
Resources
📦 DotnetFastMCP on NuGet
💻 GitHub: tekspry/DotnetFastMCP
🧪 Real-world example: FashionAccessoryPipeline
DotnetFastMCP is MIT-licensed and open for contributions.
Read original: https://dev.to/tekspry/why-your-net-mcp-tool-classes-crash-at-runtime-and-the-two-line-fix-lcb
← Previous
Learn Eval Ownership by Building a Tiny Lab Witness
Next →
NO idea where to start learning SQL?
Related
A Turing Machine in Minecraft
General
0
DEV Community
Learn Eval Ownership by Building a Tiny Lab Witness
General
3
DEV Community 周榜
5 Small Projects That Can Make You Better at JavaScript
General
3
DEV Community 周榜
How long should CLAUDE.md be? The 200-line target and what it costs you
General
0
DEV Community 周榜
Comments0
No comments yet — be the first