DEV Community

Cover image for Why Your .NET MCP Tool Classes Crash at Runtime (And the Two-Line Fix)
Gagan Bajaj
Gagan Bajaj

Posted on

Why Your .NET MCP Tool Classes Crash at Runtime (And the Two-Line Fix)

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.
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)
{
    ...
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 .
Enter fullscreen mode Exit fullscreen mode

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 is MIT-licensed and open for contributions.

Top comments (0)