cd /news/ai-agents/why-your-net-mcp-tool-classes-crash-… · home topics ai-agents article
[ARTICLE · art-128497] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

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

A developer detailed a runtime crash in .NET MCP servers where instance-based tool classes using constructor injection threw a System.Reflection.TargetException because the WithComponentsFrom method registered tool methods but never registered their declaring classes in ASP.NET Core's DI container. The fix, shipped in DotnetFastMCP v2.1, automatically registers every non-static tool class via TryAddTransient and adds parameter descriptions to the JSON Schema so AI models call tools correctly on the first attempt.

by read3 min views3 publishedSep 13, 2026

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 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.

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?

Both issues are fixed in v2.1. Install the update:

dotnet add package DotnetFastMCP --version 2.1.1

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.

[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.

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.

No breaking changes. Update the package version, remove manual AddTransient<MyToolClass>() calls (now redundant), and add [McpDescription] to your parameters.

DotnetFastMCP is MIT-licensed and open for contributions.

── more in #ai-agents 4 stories · sorted by recency
── more on @dotnetfastmcp 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/why-your-net-mcp-too…] indexed:0 read:3min 2026-09-13 ·